@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,1121 @@
1
+ /**
2
+ * OQSE v0.1 Type Definitions
3
+ * (Open Quiz & Study Exchange)
4
+ *
5
+ * Type-safe TypeScript definitions for the OQSE specification.
6
+ * Uses Discriminated Unions to make invalid states unrepresentable.
7
+ *
8
+ * @see /SPECIFICATION.md
9
+ */
10
+ /**
11
+ * BCP 47 language code (e.g., "en", "en-US", "cs", "zh-Hans")
12
+ */
13
+ export type LanguageCode = string;
14
+ /**
15
+ * SPDX license identifier (e.g., "CC-BY-SA-4.0", "CC0-1.0")
16
+ * @see https://spdx.org/licenses/
17
+ */
18
+ export type SPDXLicense = string;
19
+ /**
20
+ * ISO 8601 date/time string in RFC 3339 format.
21
+ * Examples: "2025-11-21", "2025-11-21T14:30:00Z", "2025-11-21T15:30:00+01:00"
22
+ */
23
+ export type ISO8601DateTime = string;
24
+ /**
25
+ * Media object type
26
+ */
27
+ export type MediaType = 'image' | 'audio' | 'video' | 'model';
28
+ /**
29
+ * Media object for embedding media in items or set metadata.
30
+ * Used in `meta.assets` and `item.assets`.
31
+ */
32
+ export interface MediaObject {
33
+ /** Media type */
34
+ type: MediaType;
35
+ /**
36
+ * URI of the resource.
37
+ * Can be absolute URL (https://...) or relative path in package (assets/...)
38
+ */
39
+ value: string;
40
+ /** MIME type (e.g., "image/png", "audio/mpeg", "video/mp4") */
41
+ mimeType?: string;
42
+ /**
43
+ * Alternative text for accessibility.
44
+ * REQUIRED for images, optional for audio/video.
45
+ * MUST be plain text without formatting.
46
+ */
47
+ altText?: string;
48
+ /**
49
+ * Verbatim transcript of spoken word (for audio/video).
50
+ * Rich Content (Markdown, LaTeX).
51
+ */
52
+ transcript?: string;
53
+ /**
54
+ * Media caption displayed below/next to media.
55
+ * Rich Content (Markdown, LaTeX).
56
+ */
57
+ caption?: string;
58
+ /** Preferred width in pixels (rendering hint) */
59
+ width?: number;
60
+ /** Preferred height in pixels (rendering hint) */
61
+ height?: number;
62
+ /** Time in seconds where media playback should start (for audio/video) */
63
+ start?: number;
64
+ /** Time in seconds where media playback should end (for audio/video) */
65
+ end?: number;
66
+ /** Whether media should loop (for audio/video). Default: false */
67
+ loop?: boolean;
68
+ /** Subtitles for audio/video */
69
+ subtitles?: SubtitleTrack[];
70
+ /** SPDX license for this specific media (if different from set) */
71
+ license?: SPDXLicense;
72
+ /** Media author/source attribution (e.g., "Photo: NASA") */
73
+ attribution?: string;
74
+ /**
75
+ * File integrity checksums.
76
+ * Key is algorithm name ("sha256", "sha512", "md5"), value is hex hash.
77
+ */
78
+ checksums?: Record<string, string>;
79
+ }
80
+ /**
81
+ * Subtitle track for audio/video media
82
+ */
83
+ export interface SubtitleTrack {
84
+ /** Language code (BCP 47) */
85
+ lang: LanguageCode;
86
+ /** URI to .vtt or .srt file (absolute URL or relative path) */
87
+ value: string;
88
+ /** Display name (e.g., "Czech", "English") */
89
+ label?: string;
90
+ /** Subtitle type per WebVTT specification. Default: "subtitles" */
91
+ kind?: 'captions' | 'subtitles' | 'descriptions';
92
+ }
93
+ /**
94
+ * Dictionary of media objects.
95
+ * Key is lowercase alphanumeric identifier (a-z, 0-9, _, -)
96
+ */
97
+ export type AssetDictionary = Record<string, MediaObject>;
98
+ /**
99
+ * Person object for identifying authors and contributors
100
+ */
101
+ export interface PersonObject {
102
+ /** Full name of the person */
103
+ name: string;
104
+ /** Role in the project (e.g., "Editor", "Translator", "Biology Expert") */
105
+ role?: string;
106
+ /** Contact email */
107
+ email?: string;
108
+ /** Link to website or author profile */
109
+ url?: string;
110
+ }
111
+ /**
112
+ * Source material type
113
+ */
114
+ export type SourceMaterialType = 'url' | 'doi' | 'isbn' | 'pdf' | 'textbook' | 'video' | 'audio' | 'image' | 'model' | 'other';
115
+ /**
116
+ * Source material object describing sources from which the set draws
117
+ */
118
+ export interface SourceMaterial {
119
+ /**
120
+ * Unique ID within the file (not global UUID).
121
+ * Can be alphanumeric string (e.g., "src-1", "book-physics")
122
+ */
123
+ id: string;
124
+ /** Source type */
125
+ type: SourceMaterialType;
126
+ /**
127
+ * Source value.
128
+ * For 'url', 'pdf', 'video', 'audio', 'image': absolute URL
129
+ * For 'doi', 'isbn', 'textbook', 'other': identifier string
130
+ */
131
+ value: string;
132
+ /** Source title */
133
+ title: string;
134
+ /** Additional source description */
135
+ description?: string;
136
+ /** Source authors */
137
+ authors?: string[];
138
+ /** Source publication date (ISO 8601) */
139
+ publishedDate?: ISO8601DateTime;
140
+ /** Date when the source was used (ISO 8601) */
141
+ retrievedAt?: ISO8601DateTime;
142
+ /** License of the original source (SPDX ID) */
143
+ license?: SPDXLicense;
144
+ }
145
+ /**
146
+ * Reference to a source material from an item
147
+ */
148
+ export interface SourceReference {
149
+ /** ID of source defined in meta.sourceMaterials */
150
+ id: string;
151
+ /** Specification of place in source (e.g., "page 42", "time 12:30") */
152
+ location?: string;
153
+ /**
154
+ * Exact citation or text segment from source.
155
+ * Rich Content (Markdown, LaTeX).
156
+ */
157
+ quote?: string;
158
+ }
159
+ /**
160
+ * Tag definition for semantic linking
161
+ */
162
+ export interface TagDefinition {
163
+ /** "Q" identifier from Wikidata for semantic linking */
164
+ wikidataId?: string;
165
+ /** More detailed tag description */
166
+ description?: string;
167
+ }
168
+ /**
169
+ * Dictionary of tag definitions.
170
+ * Key is the tag string, value is TagDefinition.
171
+ */
172
+ export type TagDefinitionDictionary = Record<string, TagDefinition>;
173
+ /**
174
+ * FeatureProfile is a base structure shared between:
175
+ * - Application Manifest `capabilities` object (§2.1.2)
176
+ * - Study Set `meta.requirements` object (§3)
177
+ *
178
+ * It declares which features, LaTeX packages, and item/meta properties
179
+ * an application supports (in Manifest) or requires (in Study Set).
180
+ *
181
+ * @see /SPECIFICATION.md#22-feature-profile--official-registry
182
+ */
183
+ export interface FeatureProfile {
184
+ /**
185
+ * Array of feature flags from the Official Feature Registry or `x-` prefixed extensions.
186
+ * Example: `["math", "markdown", "x-memizy-3d-voxel"]`
187
+ */
188
+ features?: string[];
189
+ /**
190
+ * Array of supported/required LaTeX packages.
191
+ * Only meaningful when `features` includes `"math"`.
192
+ * Example: `["mhchem", "amsmath"]`
193
+ */
194
+ latexPackages?: string[];
195
+ /**
196
+ * Array of item-level properties supported/required.
197
+ * Example: `["hints", "explanation", "sources"]`
198
+ */
199
+ itemProperties?: string[];
200
+ /**
201
+ * Array of set-level metadata properties supported/required.
202
+ * Example: `["license", "contributors", "tags"]`
203
+ */
204
+ metaProperties?: string[];
205
+ }
206
+ /**
207
+ * Feature requirement type
208
+ * @deprecated Use FeatureProfile instead for spec-compliant requirements.
209
+ */
210
+ export type FeatureType = 'official' | 'experimental' | 'proprietary';
211
+ /**
212
+ * Official feature names
213
+ */
214
+ export type OfficialFeatureName = 'math' | 'media-image' | 'media-audio' | 'media-video' | 'hotspots' | 'complex-pairing' | 'open-text';
215
+ /**
216
+ * Feature requirement object
217
+ */
218
+ export interface FeatureRequirement {
219
+ /** Feature name */
220
+ name: string;
221
+ /** Feature type */
222
+ type: FeatureType;
223
+ /**
224
+ * For proprietary features: Domain name of the provider (e.g., "memizy.com").
225
+ * MUST be present for type: "proprietary", ignored for other types.
226
+ */
227
+ vendor?: string;
228
+ }
229
+ /**
230
+ * Translation object defining reference to translated version of this set
231
+ */
232
+ export interface TranslationObject {
233
+ /** Language code (BCP 47) of translated set */
234
+ lang: LanguageCode;
235
+ /** Unique UUID (meta.id) of translated set */
236
+ id: string;
237
+ /** Name of translated set */
238
+ title: string;
239
+ /** Link to download translated set */
240
+ downloadUrl?: string;
241
+ }
242
+ /**
243
+ * Linked set object for prerequisites and related sets
244
+ */
245
+ export interface LinkedSetObject {
246
+ /** UUID of OQSE set being referenced */
247
+ id: string;
248
+ /** Name of referenced set */
249
+ title: string;
250
+ /** Absolute address for downloading set */
251
+ downloadUrl?: string;
252
+ }
253
+ /**
254
+ * Bloom's taxonomy level (revised)
255
+ */
256
+ export type BloomLevel = 'remember' | 'understand' | 'apply' | 'analyze' | 'evaluate' | 'create';
257
+ /**
258
+ * Cognitive load level
259
+ */
260
+ export type CognitiveLoad = 'low' | 'medium' | 'high';
261
+ /**
262
+ * Pedagogy object for advanced data for adaptive learning and psychometrics
263
+ */
264
+ export interface Pedagogy {
265
+ /** Level according to revised Bloom's taxonomy */
266
+ bloomLevel?: BloomLevel;
267
+ /** IRT Parameter b (Difficulty). Typically range -3.0 to +3.0 */
268
+ irtDifficulty?: number;
269
+ /** IRT Parameter a (Discrimination) */
270
+ irtDiscrimination?: number;
271
+ /** IRT Parameter c (Pseudo-guessing). Probability of guessing answer by chance */
272
+ irtGuessing?: number;
273
+ /** Average time (in seconds) students actually need to solve */
274
+ avgTime?: number;
275
+ /** Subjective cognitive load */
276
+ cognitiveLoad?: CognitiveLoad;
277
+ /** Whether item supports partial scoring */
278
+ partialCredit?: boolean;
279
+ /** Penalty for each wrong choice (0.0 - 1.0) */
280
+ penaltyPerWrong?: number;
281
+ }
282
+ /**
283
+ * Math renderer type
284
+ */
285
+ export type MathRenderer = 'katex' | 'mathjax';
286
+ /**
287
+ * Math settings for rendering mathematical formulas
288
+ */
289
+ export interface MathSettings {
290
+ /** Preferred renderer */
291
+ renderer?: MathRenderer;
292
+ /** Array of LaTeX package names required for proper formula display */
293
+ packages?: string[];
294
+ }
295
+ /**
296
+ * Metadata describing the entire set
297
+ */
298
+ export interface OQSEMeta {
299
+ /** Unique UUID of the set */
300
+ id: string;
301
+ /**
302
+ * Origin UUID of the creator.
303
+ * Used to verify ownership when syncing between devices or cloud.
304
+ * If created anonymously, this is the local device install ID.
305
+ */
306
+ originUid?: string;
307
+ /** Language code of the set (BCP 47) */
308
+ language: LanguageCode;
309
+ /** Title of the study set (Plain Text) */
310
+ title: string;
311
+ /** Short description of the set (Rich Content) */
312
+ description?: string;
313
+ /** Key from meta.assets that defines the cover image */
314
+ thumbnail?: string;
315
+ /** Dictionary of media for the entire set */
316
+ assets?: AssetDictionary;
317
+ /** Minimum recommended age (0 = preschool, recommended >= 3) */
318
+ ageMin?: number;
319
+ /** Maximum recommended age */
320
+ ageMax?: number;
321
+ /** Subject/field (e.g., "mathematics", "biology", "history") */
322
+ subject?: string;
323
+ /** Date and time of creation (ISO 8601) */
324
+ createdAt: ISO8601DateTime;
325
+ /** Date and time of last modification (ISO 8601) */
326
+ updatedAt: ISO8601DateTime;
327
+ /** Information about the main author of the set */
328
+ author?: PersonObject;
329
+ /** Array of information about other contributors */
330
+ contributors?: PersonObject[];
331
+ /** SPDX license identifier */
332
+ license?: SPDXLicense;
333
+ /** Link to the full text of the license */
334
+ licenseUrl?: string;
335
+ /**
336
+ * A FeatureProfile object describing set's explicit requirements.
337
+ * Declares which features, LaTeX packages, and item/meta properties are required.
338
+ * Note: Required item types and assets are implicit and MUST NOT be listed here.
339
+ *
340
+ * @see FeatureProfile
341
+ */
342
+ requirements?: FeatureProfile;
343
+ /**
344
+ * @deprecated Use `requirements` (FeatureProfile) instead.
345
+ * This field used a non-spec-compliant structure.
346
+ */
347
+ requiredFeatures?: FeatureRequirement[];
348
+ /** Array of text labels (tags) for the entire set */
349
+ tags?: string[];
350
+ /** Dictionary of tag definitions */
351
+ tagDefinitions?: TagDefinitionDictionary;
352
+ /** Array of references to translations of this set */
353
+ translations?: TranslationObject[];
354
+ /** Array of objects describing sources from which the set draws */
355
+ sourceMaterials?: SourceMaterial[];
356
+ /** Estimated time to complete the set in minutes */
357
+ estimatedTime?: number;
358
+ /** Array of references to sets that should be completed before this set */
359
+ prerequisites?: LinkedSetObject[];
360
+ /** Array of references to related OQSE sets */
361
+ relatedSets?: LinkedSetObject[];
362
+ /** Configuration for rendering mathematical formulas */
363
+ mathSettings?: MathSettings;
364
+ /** Object for metadata determined by the author/creator */
365
+ customData?: Record<string, unknown>;
366
+ /** Object for metadata specific to a particular software application */
367
+ appSpecific?: Record<string, unknown>;
368
+ }
369
+ /**
370
+ * Base properties shared by all item types
371
+ */
372
+ export interface BaseItem {
373
+ /** Unique UUID of the item */
374
+ id: string;
375
+ /** Item type (discriminator for union) */
376
+ type: string;
377
+ /** Dictionary of media for this item */
378
+ assets?: AssetDictionary;
379
+ /** Language code (BCP 47) for this specific item. Overrides meta.language */
380
+ lang?: LanguageCode;
381
+ /** Array of text labels (tags) for this item */
382
+ tags?: string[];
383
+ /** Numeric difficulty from 1 (easy) to 5 (hard) */
384
+ difficulty?: number;
385
+ /** Recommended time limit in seconds for answering this item */
386
+ timeLimit?: number;
387
+ /** Array of hints (Rich Content) */
388
+ hints?: string[];
389
+ /** Explanation of the correct answer (Rich Content) */
390
+ explanation?: string;
391
+ /**
392
+ * Message displayed after an incorrect answer (Rich Content).
393
+ * Guidance without revealing the solution.
394
+ */
395
+ incorrectFeedback?: string;
396
+ /** Array of references to sources related to this item */
397
+ sources?: SourceReference[];
398
+ /** Array of IDs of related items in this set */
399
+ relatedItems?: string[];
400
+ /** Array of IDs of items that should logically precede this item */
401
+ dependencyItems?: string[];
402
+ /** Advanced data for adaptive learning and psychometrics */
403
+ pedagogy?: Pedagogy;
404
+ /** Field for static creator metadata */
405
+ customData?: Record<string, unknown>;
406
+ /** Field for application-specific static metadata */
407
+ appSpecific?: Record<string, unknown>;
408
+ }
409
+ /**
410
+ * Select blank object for fill-in-select items
411
+ */
412
+ export interface SelectBlankObject {
413
+ /** Array of text options to choose from (Rich Content) */
414
+ options: string[];
415
+ /** Index of correct answer in options array (0-based) */
416
+ correctIndex: number;
417
+ }
418
+ /**
419
+ * Hotspot shape type.
420
+ * `rect`, `circle`, `polygon` are for 2D image hotspots (pin-on-image).
421
+ * `mesh` is for 3D model mesh selection (pin-on-model).
422
+ */
423
+ export type HotspotShape = 'rect' | 'circle' | 'polygon' | 'mesh';
424
+ /**
425
+ * Base hotspot properties
426
+ */
427
+ interface BaseHotspot {
428
+ /** Zone shape */
429
+ type: HotspotShape;
430
+ /** Zone label (Plain Text) */
431
+ label?: string;
432
+ }
433
+ /**
434
+ * Rectangle hotspot
435
+ */
436
+ export interface RectHotspot extends BaseHotspot {
437
+ type: 'rect';
438
+ /** Percent X from left edge (0-100) */
439
+ x: number;
440
+ /** Percent Y from top edge (0-100) */
441
+ y: number;
442
+ /** Width in percentages (0-100) */
443
+ width: number;
444
+ /** Height in percentages (0-100) */
445
+ height: number;
446
+ }
447
+ /**
448
+ * Circle hotspot
449
+ */
450
+ export interface CircleHotspot extends BaseHotspot {
451
+ type: 'circle';
452
+ /** Center X in percentages (0-100) */
453
+ x: number;
454
+ /** Center Y in percentages (0-100) */
455
+ y: number;
456
+ /** Radius in percentages (0-100) */
457
+ radius: number;
458
+ }
459
+ /**
460
+ * Polygon hotspot
461
+ */
462
+ export interface PolygonHotspot extends BaseHotspot {
463
+ type: 'polygon';
464
+ /** Array of points defining shape */
465
+ points: Array<{
466
+ x: number;
467
+ y: number;
468
+ }>;
469
+ }
470
+ /**
471
+ * Mesh hotspot for 3D model items (`pin-on-model`).
472
+ * Identifies a named mesh/object in a glTF scene by its node name.
473
+ * Coordinates are not used; selection is by object identity in the 3D scene.
474
+ */
475
+ export interface MeshHotspot extends BaseHotspot {
476
+ type: 'mesh';
477
+ /**
478
+ * Name (or partial name) of the object/mesh in the 3D scene.
479
+ * Corresponds to the glTF node name in the `.glb` / `.gltf` file.
480
+ * Example: `"Femur"`, `"spine_L3"`
481
+ */
482
+ targetName: string;
483
+ }
484
+ /**
485
+ * Hotspot object (Discriminated Union).
486
+ * Use `RectHotspot | CircleHotspot | PolygonHotspot` for 2D image hotspots.
487
+ * Use `MeshHotspot` for 3D model mesh selection.
488
+ */
489
+ export type HotspotObject = RectHotspot | CircleHotspot | PolygonHotspot | MeshHotspot;
490
+ /**
491
+ * A 3D point or vector with X, Y, Z coordinates.
492
+ * Used for camera setup in `pin-on-model` items.
493
+ */
494
+ export interface Vector3 {
495
+ x: number;
496
+ y: number;
497
+ z: number;
498
+ }
499
+ /**
500
+ * Recommended initial camera configuration for `pin-on-model` items.
501
+ * If omitted, the application SHOULD auto-fit the model in view.
502
+ */
503
+ export interface CameraSetup {
504
+ /** Camera position in 3D world space */
505
+ position?: Vector3;
506
+ /** The point the camera is looking at and orbiting around */
507
+ target?: Vector3;
508
+ }
509
+ /**
510
+ * Categorize item
511
+ */
512
+ export interface CategorizeItem {
513
+ /**
514
+ * Unique ID within question (not global UUID).
515
+ * Can be alphanumeric string.
516
+ */
517
+ id: string;
518
+ /** Item text (Rich Content) */
519
+ text: string;
520
+ /** 0-based index into categories array in parent item */
521
+ correctCategoryIndex: number;
522
+ }
523
+ /**
524
+ * Timeline event precision
525
+ */
526
+ export type TimelinePrecision = 'year' | 'month' | 'day' | 'datetime';
527
+ /**
528
+ * Timeline event
529
+ */
530
+ export interface TimelineEvent {
531
+ /**
532
+ * Unique ID within question (not global UUID).
533
+ * Can be alphanumeric string.
534
+ */
535
+ id: string;
536
+ /** Event description (Rich Content) */
537
+ text: string;
538
+ /**
539
+ * Event date in ISO 8601 format.
540
+ * MUST always contain full date, even if precision is only year or month.
541
+ */
542
+ date: ISO8601DateTime;
543
+ /** Determines display format to user. Default: use full precision from date */
544
+ precision?: TimelinePrecision;
545
+ }
546
+ /**
547
+ * Diagram label zone (extended hotspot with correctLabelIndex)
548
+ */
549
+ export type DiagramZone = (RectHotspot | CircleHotspot | PolygonHotspot) & {
550
+ /** Index of correct label from labels array (0-based) */
551
+ correctLabelIndex: number;
552
+ };
553
+ /**
554
+ * Rubric criterion for open-ended questions
555
+ */
556
+ export interface RubricCriterion {
557
+ /** Criterion name (e.g., "Grammar", "Argumentation") */
558
+ label: string;
559
+ /** Criterion weight in percentages (0-100) */
560
+ percentage: number;
561
+ /** Description of what is evaluated in this criterion */
562
+ description?: string;
563
+ }
564
+ /**
565
+ * Rubric for open-ended questions
566
+ */
567
+ export interface Rubric {
568
+ /** Array of criteria */
569
+ criteria: RubricCriterion[];
570
+ }
571
+ /**
572
+ * Range object for numeric-input
573
+ */
574
+ export interface NumericRange {
575
+ /** Minimum value (inclusive) */
576
+ min: number;
577
+ /** Maximum value (inclusive) */
578
+ max: number;
579
+ }
580
+ /**
581
+ * Note item (Study Note)
582
+ */
583
+ export interface NoteItem extends BaseItem {
584
+ type: 'note';
585
+ /** Note heading (Plain Text) */
586
+ title?: string;
587
+ /** Main educational content (Rich Content) */
588
+ content: string;
589
+ }
590
+ /**
591
+ * Flashcard item
592
+ */
593
+ export interface FlashcardItem extends BaseItem {
594
+ type: 'flashcard';
595
+ /** Text on front side (Rich Content) */
596
+ front: string;
597
+ /** Text on back side (Rich Content) */
598
+ back: string;
599
+ }
600
+ /**
601
+ * True/False item
602
+ */
603
+ export interface TrueFalseItem extends BaseItem {
604
+ type: 'true-false';
605
+ /** Statement to be evaluated (Rich Content) */
606
+ question: string;
607
+ /** Correct answer */
608
+ answer: boolean;
609
+ }
610
+ /**
611
+ * Multiple Choice Question - Single Choice
612
+ */
613
+ export interface MCQSingleItem extends BaseItem {
614
+ type: 'mcq-single';
615
+ /** Question text (Rich Content) */
616
+ question: string;
617
+ /** Array of text options (Rich Content). Min 2 items. */
618
+ options: string[];
619
+ /** Index (0-based) of correct answer in options array */
620
+ correctIndex: number;
621
+ /** Whether application should shuffle options. Default: true */
622
+ shuffleOptions?: boolean;
623
+ /**
624
+ * Array of explanations specific to each option (Rich Content).
625
+ * Index corresponds to index in options array.
626
+ * Use null for options without specific explanation.
627
+ */
628
+ optionExplanations?: Array<string | null>;
629
+ }
630
+ /**
631
+ * Multiple Choice Question - Multiple Choice
632
+ */
633
+ export interface MCQMultiItem extends BaseItem {
634
+ type: 'mcq-multi';
635
+ /** Question text (Rich Content) */
636
+ question: string;
637
+ /** Array of text options (Rich Content). Min 2 items. */
638
+ options: string[];
639
+ /** Array of indices of correct answers. Min 1 index. */
640
+ correctIndices: number[];
641
+ /** Minimum number of answers user must select */
642
+ minSelections?: number;
643
+ /** Maximum number of answers user can select */
644
+ maxSelections?: number;
645
+ /** Whether application should shuffle options. Default: true */
646
+ shuffleOptions?: boolean;
647
+ /**
648
+ * Array of explanations specific to each option (Rich Content).
649
+ * Index corresponds to index in options array.
650
+ * Use null for options without specific explanation.
651
+ */
652
+ optionExplanations?: Array<string | null>;
653
+ }
654
+ /**
655
+ * Short Text Answer
656
+ */
657
+ export interface ShortAnswerItem extends BaseItem {
658
+ type: 'short-answer';
659
+ /** Question text (Rich Content) */
660
+ question: string;
661
+ /** Array of acceptable text answers (Plain Text). Min 1 item. */
662
+ answers: string[];
663
+ /** Distinguish letter case. Default: false */
664
+ caseSensitive?: boolean;
665
+ /** Ignore spaces at beginning/end. Default: true */
666
+ trimWhitespace?: boolean;
667
+ /** Accept approximate match (fuzzy matching). Default: false */
668
+ acceptPartial?: boolean;
669
+ /**
670
+ * Remove diacritics before comparison.
671
+ * Default: false
672
+ */
673
+ ignoreDiacritics?: boolean;
674
+ }
675
+ /**
676
+ * Fill in the Blanks
677
+ */
678
+ export interface FillInBlanksItem extends BaseItem {
679
+ type: 'fill-in-blanks';
680
+ /** Heading or instructions (Rich Content) */
681
+ question?: string;
682
+ /**
683
+ * Text with blanks marked using blank tags <blank:token />.
684
+ * Rich Content (Markdown, LaTeX, Media Tags).
685
+ */
686
+ text: string;
687
+ /**
688
+ * Dictionary where key is token and value is array of correct answers (Plain Text).
689
+ * Min 1 token.
690
+ */
691
+ blanks: Record<string, string[]>;
692
+ /** Distinguish letter case when checking answers. Default: false */
693
+ caseSensitive?: boolean;
694
+ /** Ignore spaces at beginning and end of answer. Default: true */
695
+ trimWhitespace?: boolean;
696
+ }
697
+ /**
698
+ * Fill in with Selection
699
+ */
700
+ export interface FillInSelectItem extends BaseItem {
701
+ type: 'fill-in-select';
702
+ /** Heading or instructions (Rich Content) */
703
+ question?: string;
704
+ /**
705
+ * Text with blanks marked using blank tags <blank:token />.
706
+ * Rich Content (Markdown, LaTeX, Media Tags).
707
+ */
708
+ text: string;
709
+ /**
710
+ * Dictionary where key is token and value is SelectBlankObject.
711
+ * Min 1 token.
712
+ */
713
+ blanks: Record<string, SelectBlankObject>;
714
+ }
715
+ /**
716
+ * Match Pairs
717
+ */
718
+ export interface MatchPairsItem extends BaseItem {
719
+ type: 'match-pairs';
720
+ /** Instructions (Rich Content) */
721
+ question?: string;
722
+ /** Left side (what is being matched) - array of strings (Rich Content). Min 2 items. */
723
+ prompts: string[];
724
+ /**
725
+ * Right side (target) - array of strings (Rich Content).
726
+ * Must have same length as prompts. Min 2 items.
727
+ * prompts[0] belongs to matches[0], etc.
728
+ */
729
+ matches: string[];
730
+ }
731
+ /**
732
+ * Advanced Matching
733
+ */
734
+ export interface MatchComplexItem extends BaseItem {
735
+ type: 'match-complex';
736
+ /** Instructions (Rich Content) */
737
+ question?: string;
738
+ /** Items on left side (Rich Content). Min 1 item. */
739
+ leftItems: string[];
740
+ /** Items on right side (Rich Content). Min 1 item. */
741
+ rightItems: string[];
742
+ /**
743
+ * Array of index pairs [left_index, right_index] defining correct pairs.
744
+ * Min 1 connection.
745
+ */
746
+ connections: Array<[number, number]>;
747
+ /** Minimum number of correct connections required for success */
748
+ minCorrect?: number;
749
+ }
750
+ /**
751
+ * Sorting
752
+ */
753
+ export interface SortItemsItem extends BaseItem {
754
+ type: 'sort-items';
755
+ /** Instructions (Rich Content) */
756
+ question: string;
757
+ /**
758
+ * Array of items in correct order (Rich Content).
759
+ * Min 2 items. Application must shuffle them.
760
+ */
761
+ items: string[];
762
+ }
763
+ /**
764
+ * Slider / Numeric Answer
765
+ */
766
+ export interface SliderItem extends BaseItem {
767
+ type: 'slider';
768
+ /** Question text (Rich Content) */
769
+ question: string;
770
+ /** Minimum value on slider */
771
+ min: number;
772
+ /** Maximum value on slider */
773
+ max: number;
774
+ /** Slider step (e.g., 1, 0.1). Must be > 0 */
775
+ step: number;
776
+ /** Correct value. Must be reachable by slider. */
777
+ correctAnswer: number;
778
+ /** Allowed deviation (can be 0) */
779
+ tolerance: number;
780
+ /** Unit (e.g., "year", "m", "°C") (Plain Text) */
781
+ unit?: string;
782
+ }
783
+ /**
784
+ * Pin on Image
785
+ */
786
+ export interface PinOnImageItem extends BaseItem {
787
+ type: 'pin-on-image';
788
+ /** Instructions (Rich Content) */
789
+ question: string;
790
+ /** Key from assets, which determines the image to be clicked on */
791
+ targetAsset: string;
792
+ /** Array defining correct areas. Min 1 hotspot. */
793
+ hotspots: HotspotObject[];
794
+ /** Whether user must mark more than one hotspot. Default: false */
795
+ multipleCorrect?: boolean;
796
+ /**
797
+ * If multipleCorrect: true, determines minimum number of correct hotspots.
798
+ * Ignored if multipleCorrect: false
799
+ */
800
+ minCorrect?: number;
801
+ }
802
+ /**
803
+ * Categorization
804
+ */
805
+ export interface CategorizeItemType extends BaseItem {
806
+ type: 'categorize';
807
+ /** Instructions (Rich Content) */
808
+ question: string;
809
+ /** Array of category names (Plain Text). Min 2 items. */
810
+ categories: string[];
811
+ /** Items to sort. Min 1 item. */
812
+ items: CategorizeItem[];
813
+ }
814
+ /**
815
+ * Timeline
816
+ */
817
+ export interface TimelineItem extends BaseItem {
818
+ type: 'timeline';
819
+ /** Instructions (Rich Content) */
820
+ question: string;
821
+ /** Array of events in correct chronological order. Min 2 events. */
822
+ events: TimelineEvent[];
823
+ /** Whether application should shuffle events. Default: true */
824
+ randomize?: boolean;
825
+ }
826
+ /**
827
+ * Matrix / Table Answer
828
+ */
829
+ export interface MatrixItem extends BaseItem {
830
+ type: 'matrix';
831
+ /** Instructions (Rich Content) */
832
+ question: string;
833
+ /** Row labels (Plain Text). Min 1 item. */
834
+ rows: string[];
835
+ /** Column labels (Plain Text). Min 1 item. */
836
+ columns: string[];
837
+ /**
838
+ * Array of coordinates of correct answers in format [row, column].
839
+ * Indices are 0-based. Min 1 cell.
840
+ */
841
+ correctCells: Array<[number, number]>;
842
+ /** Can user select multiple answers in one row? Default: false */
843
+ multiplePerRow?: boolean;
844
+ }
845
+ /**
846
+ * Math Input
847
+ */
848
+ export interface MathInputItem extends BaseItem {
849
+ type: 'math-input';
850
+ /** Question text (Rich Content) */
851
+ question: string;
852
+ /** Correct answer in LaTeX format (e.g., "$2x + 2$") */
853
+ correctAnswer: string;
854
+ /** Array of other text strings that should also be considered correct */
855
+ alternativeAnswers?: string[];
856
+ /** For purely numeric answers, allowed numeric deviation */
857
+ tolerance?: number;
858
+ }
859
+ /**
860
+ * Diagram Labeling
861
+ */
862
+ export interface DiagramLabelItem extends BaseItem {
863
+ type: 'diagram-label';
864
+ /** Instructions (Rich Content) */
865
+ question: string;
866
+ /** Key from assets, which determines the diagram image */
867
+ targetAsset: string;
868
+ /**
869
+ * Array of text labels for user to assign (Rich Content).
870
+ * Must contain all correct answers and may contain distractors.
871
+ */
872
+ labels: string[];
873
+ /** Distinguish letter case when comparing labels. Default: false */
874
+ caseSensitive?: boolean;
875
+ /**
876
+ * If true, application MUST render text fields instead of draggable labels.
877
+ * Default: false
878
+ */
879
+ requireTyping?: boolean;
880
+ /** Array of zones on image. Min 1 zone. */
881
+ zones: DiagramZone[];
882
+ }
883
+ /**
884
+ * Open-Ended Answer
885
+ */
886
+ export interface OpenEndedItem extends BaseItem {
887
+ type: 'open-ended';
888
+ /** Question text (Rich Content) */
889
+ question: string;
890
+ /** Minimum required word count */
891
+ minWords?: number;
892
+ /** Maximum allowed word count */
893
+ maxWords?: number;
894
+ /** Sample (model) answer for evaluators (Rich Content) */
895
+ sampleAnswer?: string;
896
+ /** Structured evaluation criteria */
897
+ rubric?: Rubric;
898
+ }
899
+ /**
900
+ * Numeric Input
901
+ */
902
+ export interface NumericInputItem extends BaseItem {
903
+ type: 'numeric-input';
904
+ /** Question text (Rich Content) */
905
+ question: string;
906
+ /** Correct numeric value (float) */
907
+ value: number;
908
+ /** Absolute allowed deviation. Default: 0 */
909
+ tolerance?: number;
910
+ /**
911
+ * Alternative to value+tolerance.
912
+ * If specified, takes precedence over value.
913
+ */
914
+ range?: NumericRange;
915
+ /** Unit displayed after input field (Plain Text) */
916
+ unit?: string;
917
+ }
918
+ /**
919
+ * Pin on 3D Model
920
+ *
921
+ * The user must locate and click on a specific named part (mesh) of a 3D model.
922
+ * Requires the `capabilities.assets.model` to be declared in the application manifest.
923
+ *
924
+ * @see §6.21
925
+ */
926
+ export interface PinOnModelItem extends BaseItem {
927
+ type: 'pin-on-model';
928
+ /** Instructions (Rich Content) */
929
+ question: string;
930
+ /**
931
+ * Key from `assets` dictionary pointing to the 3D model.
932
+ * The referenced asset MUST have `type: "model"` and use glTF/GLB format.
933
+ * Example key: `"model_lower_limb"`
934
+ */
935
+ targetAsset: string;
936
+ /**
937
+ * Array of correct mesh hotspots. At least 1 hotspot is required.
938
+ * For 3D models, use `MeshHotspot` (`type: "mesh"`).
939
+ */
940
+ hotspots: MeshHotspot[];
941
+ /**
942
+ * Whether the user must find more than one target mesh.
943
+ * Default: `false`
944
+ */
945
+ multipleCorrect?: boolean;
946
+ /**
947
+ * Minimum number of correct meshes the user must find.
948
+ * Only applies when `multipleCorrect: true`.
949
+ */
950
+ minCorrect?: number;
951
+ /**
952
+ * Recommended initial camera setup.
953
+ * If omitted, the application SHOULD auto-fit the entire model in view.
954
+ */
955
+ camera?: CameraSetup;
956
+ }
957
+ /**
958
+ * Chess Puzzle
959
+ *
960
+ * An interactive chess puzzle where the user finds the correct move sequence
961
+ * on a given board position. Applications SHOULD render an interactive chessboard.
962
+ *
963
+ * @see §6.22
964
+ */
965
+ export interface ChessPuzzleItem extends BaseItem {
966
+ type: 'chess-puzzle';
967
+ /**
968
+ * Instructions or puzzle description (Rich Content).
969
+ * Example: `"White to move and checkmate in 2."`
970
+ */
971
+ question: string;
972
+ /**
973
+ * Board position in Forsyth–Edwards Notation (FEN).
974
+ * Example: `"r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4"`
975
+ */
976
+ fen: string;
977
+ /**
978
+ * Array of valid solution move sequences.
979
+ * Each sequence is an array of moves in Standard Algebraic Notation (SAN).
980
+ * Multiple sequences allow declaring alternative correct solutions.
981
+ * Example: `[["Ng5"]]` or `[["e4", "e5"]]`
982
+ */
983
+ answers: string[][];
984
+ /**
985
+ * ELO difficulty rating of the puzzle.
986
+ * Optional. Allows adaptive selection of puzzles by difficulty.
987
+ */
988
+ elo?: number;
989
+ }
990
+ /**
991
+ * OQSE Item (Discriminated Union)
992
+ *
993
+ * This is the power of TypeScript's Discriminated Unions:
994
+ * TypeScript will narrow the type based on the `type` field,
995
+ * making invalid states unrepresentable.
996
+ */
997
+ export type OQSEItem = NoteItem | FlashcardItem | TrueFalseItem | MCQSingleItem | MCQMultiItem | ShortAnswerItem | FillInBlanksItem | FillInSelectItem | MatchPairsItem | MatchComplexItem | SortItemsItem | SliderItem | PinOnImageItem | PinOnModelItem | CategorizeItemType | TimelineItem | MatrixItem | MathInputItem | DiagramLabelItem | OpenEndedItem | NumericInputItem | ChessPuzzleItem;
998
+ /**
999
+ * OQSE File (Root Structure)
1000
+ *
1001
+ * This is the top-level structure of an OQSE file.
1002
+ */
1003
+ export interface OQSEFile {
1004
+ /** URL reference to the JSON Schema specification */
1005
+ $schema?: string;
1006
+ /** Version of the OQSE specification (e.g., "0.1") */
1007
+ version: string;
1008
+ /** Metadata about the entire set */
1009
+ meta: OQSEMeta;
1010
+ /** Array containing individual study items */
1011
+ items: OQSEItem[];
1012
+ }
1013
+ /**
1014
+ * Type guard for NoteItem
1015
+ */
1016
+ export declare function isNote(item: OQSEItem): item is NoteItem;
1017
+ /**
1018
+ * Type guard for FlashcardItem
1019
+ */
1020
+ export declare function isFlashcard(item: OQSEItem): item is FlashcardItem;
1021
+ /**
1022
+ * Type guard for TrueFalseItem
1023
+ */
1024
+ export declare function isTrueFalse(item: OQSEItem): item is TrueFalseItem;
1025
+ /**
1026
+ * Type guard for MCQSingleItem
1027
+ */
1028
+ export declare function isMCQSingle(item: OQSEItem): item is MCQSingleItem;
1029
+ /**
1030
+ * Type guard for MCQMultiItem
1031
+ */
1032
+ export declare function isMCQMulti(item: OQSEItem): item is MCQMultiItem;
1033
+ /**
1034
+ * Type guard for ShortAnswerItem
1035
+ */
1036
+ export declare function isShortAnswer(item: OQSEItem): item is ShortAnswerItem;
1037
+ /**
1038
+ * Type guard for FillInBlanksItem
1039
+ */
1040
+ export declare function isFillInBlanks(item: OQSEItem): item is FillInBlanksItem;
1041
+ /**
1042
+ * Type guard for FillInSelectItem
1043
+ */
1044
+ export declare function isFillInSelect(item: OQSEItem): item is FillInSelectItem;
1045
+ /**
1046
+ * Type guard for MatchPairsItem
1047
+ */
1048
+ export declare function isMatchPairs(item: OQSEItem): item is MatchPairsItem;
1049
+ /**
1050
+ * Type guard for MatchComplexItem
1051
+ */
1052
+ export declare function isMatchComplex(item: OQSEItem): item is MatchComplexItem;
1053
+ /**
1054
+ * Type guard for SortItemsItem
1055
+ */
1056
+ export declare function isSortItems(item: OQSEItem): item is SortItemsItem;
1057
+ /**
1058
+ * Type guard for SliderItem
1059
+ */
1060
+ export declare function isSlider(item: OQSEItem): item is SliderItem;
1061
+ /**
1062
+ * Type guard for PinOnImageItem
1063
+ */
1064
+ export declare function isPinOnImage(item: OQSEItem): item is PinOnImageItem;
1065
+ /**
1066
+ * Type guard for CategorizeItemType
1067
+ */
1068
+ export declare function isCategorize(item: OQSEItem): item is CategorizeItemType;
1069
+ /**
1070
+ * Type guard for TimelineItem
1071
+ */
1072
+ export declare function isTimeline(item: OQSEItem): item is TimelineItem;
1073
+ /**
1074
+ * Type guard for MatrixItem
1075
+ */
1076
+ export declare function isMatrix(item: OQSEItem): item is MatrixItem;
1077
+ /**
1078
+ * Type guard for MathInputItem
1079
+ */
1080
+ export declare function isMathInput(item: OQSEItem): item is MathInputItem;
1081
+ /**
1082
+ * Type guard for DiagramLabelItem
1083
+ */
1084
+ export declare function isDiagramLabel(item: OQSEItem): item is DiagramLabelItem;
1085
+ /**
1086
+ * Type guard for OpenEndedItem
1087
+ */
1088
+ export declare function isOpenEnded(item: OQSEItem): item is OpenEndedItem;
1089
+ /**
1090
+ * Type guard for NumericInputItem
1091
+ */
1092
+ export declare function isNumericInput(item: OQSEItem): item is NumericInputItem;
1093
+ /**
1094
+ * Type guard for PinOnModelItem
1095
+ */
1096
+ export declare function isPinOnModel(item: OQSEItem): item is PinOnModelItem;
1097
+ /**
1098
+ * Type guard for ChessPuzzleItem
1099
+ */
1100
+ export declare function isChessPuzzle(item: OQSEItem): item is ChessPuzzleItem;
1101
+ /**
1102
+ * Core Item Types (Level 1: Core Consumer)
1103
+ *
1104
+ * These are the absolute minimum types that every implementation must support.
1105
+ */
1106
+ export type CoreItemType = 'note' | 'flashcard' | 'mcq-single' | 'short-answer';
1107
+ /**
1108
+ * Extended Item Types
1109
+ *
1110
+ * All other types beyond the core.
1111
+ */
1112
+ export type ExtendedItemType = Exclude<OQSEItem['type'], CoreItemType>;
1113
+ /**
1114
+ * Type guard for Core Item
1115
+ */
1116
+ export declare function isCoreItem(item: OQSEItem): boolean;
1117
+ /**
1118
+ * Type guard for Extended Item
1119
+ */
1120
+ export declare function isExtendedItem(item: OQSEItem): boolean;
1121
+ export {};