@memizy/oqse 1.0.0-rc.5 → 1.0.0-rc.6
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.
- package/CHANGELOG.md +16 -0
- package/README.md +3 -1
- package/dist/checkAnswer.cjs +1 -1
- package/dist/checkAnswer.js +1 -1
- package/dist/index.d.ts +20 -20
- package/dist/oqseValidation.cjs +1 -1
- package/dist/oqseValidation.js +23 -25
- package/dist/richTextFields.cjs +1 -1
- package/dist/richTextFields.js +1 -1
- package/dist/src/assets.d.ts +1 -1
- package/dist/src/checkAnswer.d.ts +1 -1
- package/dist/src/compatibility.d.ts +2 -2
- package/dist/src/features.d.ts +1 -1
- package/dist/src/fileValidation.d.ts +1 -1
- package/dist/src/header.d.ts +1 -1
- package/dist/src/headerValidation.d.ts +1 -1
- package/dist/src/index.d.ts +20 -20
- package/dist/src/manifest.d.ts +2 -2
- package/dist/src/manifestValidation.d.ts +1 -1
- package/dist/src/markdownSet.d.ts +2 -2
- package/dist/src/oqse.d.ts +7 -11
- package/dist/src/oqseValidation.d.ts +13 -21
- package/dist/src/plainText.d.ts +1 -1
- package/dist/src/progressValidation.d.ts +1 -1
- package/dist/src/richTextFields.d.ts +1 -1
- package/dist/src/richTextProcessor.d.ts +1 -1
- package/oqse-header.md +6 -0
- package/oqse-manifest.md +6 -0
- package/oqse-progress.md +7 -1
- package/oqse.md +39 -26
- package/package.json +14 -2
- package/schemas/oqse-v1.0.json +7 -13
- package/dist/scripts/fix-types-entry.d.ts +0 -1
- package/dist/scripts/generate-schemas.d.ts +0 -1
- package/dist/src/answers.test.d.ts +0 -1
- package/dist/src/checkAnswer.test.d.ts +0 -1
- package/dist/src/compatibility.test.d.ts +0 -1
- package/dist/src/features.test.d.ts +0 -1
- package/dist/src/fileValidation.test.d.ts +0 -1
- package/dist/src/headerValidation.test.d.ts +0 -1
- package/dist/src/manifestValidation.test.d.ts +0 -1
- package/dist/src/markdownSet.test.d.ts +0 -1
- package/dist/src/oqseValidation.test.d.ts +0 -1
- package/dist/src/plainText.test.d.ts +0 -1
- package/dist/src/progressValidation.test.d.ts +0 -1
- package/dist/src/richTextProcessor.test.d.ts +0 -1
- package/dist/src/specExamples.test.d.ts +0 -5
- package/dist/src/utils.test.d.ts +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog of @memizy/oqse
|
|
2
|
+
|
|
3
|
+
The format itself has its own changelog at the end of [oqse.md](https://github.com/memizy/memizy/blob/main/packages/oqse/oqse.md#version-10-2026-10-14) (OQSE 1.0, 2026-10-14). This file lists the releases of the npm package.
|
|
4
|
+
|
|
5
|
+
## 1.0.0-rc.6 (2026-10-14)
|
|
6
|
+
|
|
7
|
+
Release candidate of OQSE 1.0; the format is frozen (1.x only adds).
|
|
8
|
+
|
|
9
|
+
* `HotspotObject`: the shape fields (`x`, `y`, `width`, `height`, `radius`, `points`, `targetName`) are marked required in the specification, as the validators always treated them.
|
|
10
|
+
* New section "Canonical and Authoring Formats" in `oqse.md`: the JSON file is the canonical form that applications store and exchange; authoring formats (OQSE Markdown for notes, tool-specific shorthands) are only inputs converted to it, and item IDs survive edits.
|
|
11
|
+
* Aligned before 1.0: `math-input` has `correctAnswers` (a list, like `short-answer`) instead of `correctAnswer` + `alternativeAnswers`, and no `tolerance` (answers are compared as LaTeX; numbers are `numeric-input`); `match-complex` has `prompts` / `matches` (like `match-pairs`) instead of `leftItems` / `rightItems`. `checkAnswer` and the JSON Schemas follow.
|
|
12
|
+
* The sub-specifications (`oqse-manifest.md`, `oqse-progress.md`, `oqse-header.md`) point to the changelog in `oqse.md`.
|
|
13
|
+
|
|
14
|
+
## 1.0.0-rc.5
|
|
15
|
+
|
|
16
|
+
OQSE 1.0: choices with IDs, answers by ID, `skills`, Answer Formats and `checkAnswer`, typed-answer comparison, timeline rules, feature detection, version rules (0.x drafts are refused) and the JSON Schemas at `https://cdn.jsdelivr.net/npm/@memizy/oqse@1/schemas/`. See the "Version 1.0" changelog in `oqse.md`.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript SDK for the Open Quiz & Study Exchange ecosystem, published as @memizy/oqse.
|
|
4
4
|
|
|
5
|
-
🚀 **[OQSE Web Validator](https://memizy.github.io/memizy/)** - Online tool for easy testing and debugging of OQSE JSON structures in the browser (source: [`docs/`](
|
|
5
|
+
🚀 **[OQSE Web Validator](https://memizy.github.io/memizy/)** - Online tool for easy testing and debugging of OQSE JSON structures in the browser (source: [`docs/`](https://github.com/memizy/memizy/blob/main/packages/oqse/docs/), published by GitHub Pages).
|
|
6
6
|
|
|
7
7
|
This repository contains:
|
|
8
8
|
- The human-readable specifications split into four standalone documents:
|
|
@@ -36,6 +36,8 @@ const { success, data, errors, warnings } = loadOQSEFile(JSON.parse(text));
|
|
|
36
36
|
|
|
37
37
|
## Schemas
|
|
38
38
|
|
|
39
|
+
The exported `*Schema` objects are **Zod 4** schemas (use them with `.safeParse`); the 1.x line of this package stays on Zod 4.
|
|
40
|
+
|
|
39
41
|
The auto-generated JSON Schemas are published with this repository and are available at the following URLs:
|
|
40
42
|
|
|
41
43
|
- Core OQSE Schema: https://cdn.jsdelivr.net/npm/@memizy/oqse@1/schemas/oqse-v1.0.json
|
package/dist/checkAnswer.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./answers.cjs");function t(t,p){if(!t||typeof t!=`object`)throw Error(`checkAnswer: missing item`);switch(t.type){case`mcq-single`:return p===t.correctId;case`mcq-multi`:{let e=i(p);return e!==null&&c(e,t.correctIds)}case`true-false`:return r(p)===t.correctAnswer;case`short-answer`:{let n=p;return typeof n==`string`&&t.correctAnswers.some(r=>e.textAnswerMatches(n,r,t))}case`numeric-input`:{let e=n(p);return e!==null&&Math.abs(e-t.correctAnswer)<=(t.tolerance??0)+1e-9}case`slider`:{let e=n(p);return e!==null&&Math.abs(e-t.correctAnswer)<=(t.tolerance??0)+1e-9}case`math-input`:{if(typeof p!=`string`)return!1;let e=l(p);return
|
|
1
|
+
const e=require("./answers.cjs");function t(t,p){if(!t||typeof t!=`object`)throw Error(`checkAnswer: missing item`);switch(t.type){case`mcq-single`:return p===t.correctId;case`mcq-multi`:{let e=i(p);return e!==null&&c(e,t.correctIds)}case`true-false`:return r(p)===t.correctAnswer;case`short-answer`:{let n=p;return typeof n==`string`&&t.correctAnswers.some(r=>e.textAnswerMatches(n,r,t))}case`numeric-input`:{let e=n(p);return e!==null&&Math.abs(e-t.correctAnswer)<=(t.tolerance??0)+1e-9}case`slider`:{let e=n(p);return e!==null&&Math.abs(e-t.correctAnswer)<=(t.tolerance??0)+1e-9}case`math-input`:{if(typeof p!=`string`)return!1;let e=l(p);return t.correctAnswers.some(t=>l(t)===e)}case`sort-items`:{let e=i(p);return e!==null&&e.length===t.correctOrder.length&&e.every((e,n)=>e===t.correctOrder[n])}case`match-pairs`:{let e=s(p);return!!e&&t.prompts.every(n=>e[n.id]===t.pairs[n.id])}case`match-complex`:{let e=a(p);if(!e)return!1;let n=new Set(t.connections.map(([e,t])=>o(e,t))),r=new Set(e.map(([e,t])=>o(e,t))),i=[...r].filter(e=>n.has(e)).length;return i===r.size&&i>=(t.minCorrect??n.size)}case`matrix`:{let e=a(p);return e?c(e.map(([e,t])=>o(e,t)),t.correctCells.map(([e,t])=>o(e,t))):!1}case`fill-in-blanks`:{let n=s(p);return n?Object.entries(t.blanks).every(([r,i])=>{let a=n[r];return typeof a==`string`&&i.some(n=>e.textAnswerMatches(a,n,t))}):!1}case`fill-in-select`:{let e=s(p);return!!e&&Object.entries(t.blanks).every(([t,n])=>e[t]===n.correctId)}case`categorize`:{let e=s(p);return!!e&&t.items.every(t=>e[t.id]===t.correctCategoryId)}case`timeline`:{let n=i(p);if(!n||n.length!==t.events.length||new Set(n).size!==n.length)return!1;let r=n.map(e=>t.events.find(t=>t.id===e));return r.every(e=>!!e)&&e.isTimelineOrderCorrect(r)}case`pin-on-image`:{let e=(Array.isArray(p)?p:[p]).map(d);if(e.length===0||e.some(e=>e===null))return!1;if(!t.multipleCorrect)return e.length===1&&t.hotspots.some(t=>f(e[0],t));let n=new Set;for(let r of e){let e=t.hotspots.findIndex(e=>f(r,e));if(e===-1)return!1;n.add(e)}return n.size>=(t.minCorrect??t.hotspots.length)}case`diagram-label`:{let n=s(p);return n?t.zones.every(r=>{let i=n[r.id];if(t.requireTyping){let n=t.labels.find(e=>e.id===r.correctLabelId);return!!n&&typeof i==`string`&&e.textAnswerMatches(i,n.text,t)}return i===r.correctLabelId}):!1}case`pin-on-model`:{let e=(Array.isArray(p)?p:[p]).filter(e=>typeof e==`string`);if(e.length===0)return!1;let n=e=>t.hotspots.findIndex(t=>e.toLowerCase().includes(t.targetName.toLowerCase()));if(!t.multipleCorrect)return e.length===1&&n(e[0])!==-1;let r=new Set;for(let t of e){let e=n(t);if(e===-1)return!1;r.add(e)}return r.size>=(t.minCorrect??t.hotspots.length)}case`chess-puzzle`:{if(!Array.isArray(p)||p.some(e=>typeof e!=`string`))return!1;let e=p.map(u);return t.correctAnswers.some(t=>t.length===e.length&&t.every((t,n)=>u(t)===e[n]))}case`flashcard`:{let e=n(p);return e===3||e===4}case`note`:case`open-ended`:throw Error(`checkAnswer: "${t.type}" items cannot be checked automatically.`);default:throw Error(`checkAnswer: unsupported item type "${t.type}".`)}}function n(e){if(typeof e==`number`)return Number.isFinite(e)?e:null;if(typeof e!=`string`)return null;let t=e.trim().replace(/\s+/g,``).replace(`,`,`.`);return/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)?Number(t):null}function r(e){return typeof e==`boolean`?e:e===`true`||e!==`false`&&null}function i(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)?e:null}function a(e){return Array.isArray(e)&&e.every(e=>Array.isArray(e)&&e.length===2&&typeof e[0]==`string`&&typeof e[1]==`string`)?e:null}var o=(e,t)=>`${e}\u0000${t}`;function s(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:null}function c(e,t){let n=new Set(e),r=new Set(t);return n.size===e.length&&n.size===r.size&&[...n].every(e=>r.has(e))}function l(e){return e.trim().replace(/^\$\$?|\$\$?$/g,``).replace(/\s+/g,``)}function u(e){return e.trim().replace(/[+#!?]+$/g,``).replace(/^0-0(-0)?$/,e=>e.replace(/0/g,`O`)).replace(/^([a-h](?:x[a-h])?[18])([QRBN])$/,`$1=$2`)}function d(e){let t=s(e);return t&&typeof t.x==`number`&&typeof t.y==`number`?{x:t.x,y:t.y}:null}function f(e,t){switch(t.type){case`rect`:return e.x>=t.x&&e.x<=t.x+t.width&&e.y>=t.y&&e.y<=t.y+t.height;case`circle`:return(e.x-t.x)**2+(e.y-t.y)**2<=t.radius**2;case`polygon`:{let n=!1,r=t.points;for(let t=0,i=r.length-1;t<r.length;i=t++)r[t].y>e.y!=r[i].y>e.y&&e.x<(r[i].x-r[t].x)*(e.y-r[t].y)/(r[i].y-r[t].y)+r[t].x&&(n=!n);return n}}}exports.checkAnswer=t;
|
package/dist/checkAnswer.js
CHANGED
|
@@ -24,7 +24,7 @@ function n(n, m) {
|
|
|
24
24
|
case "math-input": {
|
|
25
25
|
if (typeof m != "string") return !1;
|
|
26
26
|
let e = u(m);
|
|
27
|
-
return
|
|
27
|
+
return n.correctAnswers.some((t) => u(t) === e);
|
|
28
28
|
}
|
|
29
29
|
case "sort-items": {
|
|
30
30
|
let e = a(m);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
export * from './src/oqse';
|
|
2
|
-
export * from './src/oqseValidation';
|
|
3
|
-
export * from './src/manifest';
|
|
4
|
-
export * from './src/manifestValidation';
|
|
5
|
-
export * from './src/progress';
|
|
6
|
-
export * from './src/progressValidation';
|
|
7
|
-
export * from './src/header';
|
|
8
|
-
export * from './src/headerValidation';
|
|
9
|
-
export * from './src/utils';
|
|
10
|
-
export * from './src/richTextProcessor';
|
|
11
|
-
export * from './src/markdownSet';
|
|
12
|
-
export { shiftHeadings } from './src/markdownUtils';
|
|
13
|
-
export * from './src/fileValidation';
|
|
14
|
-
export * from './src/richTextFields';
|
|
15
|
-
export * from './src/assets';
|
|
16
|
-
export * from './src/answers';
|
|
17
|
-
export * from './src/checkAnswer';
|
|
18
|
-
export * from './src/plainText';
|
|
19
|
-
export * from './src/features';
|
|
20
|
-
export * from './src/compatibility';
|
|
1
|
+
export * from './src/oqse.js';
|
|
2
|
+
export * from './src/oqseValidation.js';
|
|
3
|
+
export * from './src/manifest.js';
|
|
4
|
+
export * from './src/manifestValidation.js';
|
|
5
|
+
export * from './src/progress.js';
|
|
6
|
+
export * from './src/progressValidation.js';
|
|
7
|
+
export * from './src/header.js';
|
|
8
|
+
export * from './src/headerValidation.js';
|
|
9
|
+
export * from './src/utils.js';
|
|
10
|
+
export * from './src/richTextProcessor.js';
|
|
11
|
+
export * from './src/markdownSet.js';
|
|
12
|
+
export { shiftHeadings } from './src/markdownUtils.js';
|
|
13
|
+
export * from './src/fileValidation.js';
|
|
14
|
+
export * from './src/richTextFields.js';
|
|
15
|
+
export * from './src/assets.js';
|
|
16
|
+
export * from './src/answers.js';
|
|
17
|
+
export * from './src/checkAnswer.js';
|
|
18
|
+
export * from './src/plainText.js';
|
|
19
|
+
export * from './src/features.js';
|
|
20
|
+
export * from './src/compatibility.js';
|
package/dist/oqseValidation.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require("./utils.cjs"),t=require("./answers.cjs");let n=require("zod");var r=n.z.string().uuid({message:`Invalid UUID format`}),i=n.z.string().min(2,`Language code must have at least 2 characters`).regex(/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/,`Invalid BCP 47 format (e.g. "cs", "en-US")`),a=n.z.string().min(1,`License identifier must not be empty`),o=n.z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/,`Invalid ISO 8601 format (e.g. "2025-11-21T14:30:00Z")`),s=n.z.string().regex(/^-?\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/,`Invalid date (e.g. "1969-07-20", or "-0480-08-01" for 480 BC)`).refine(t.isTimelineDate,`Year 0000 does not exist (1 BC is "-0001", AD 1 is "0001")`),c=n.z.string().url({message:`Must be a valid absolute URL`}),l=n.z.string().regex(/^[a-z0-9_-]+$/,`Asset key must contain only lowercase letters, numbers, hyphens and underscores`),u=n.z.string().refine(e=>e.trim().length>0,`Text must not be empty`).refine(e=>!/<asset:[^>]*>/i.test(e),`Plain Text fields must not contain media tags (<asset:key />)`),d=u.refine(e=>!/^\s*\$|\$\s*$/.test(e),`LaTeX answer must not be wrapped in $ delimiters (write "2x + 2", not "$2x + 2$")`),f=n.z.string().min(1,`Content must not be empty`),p=n.z.string().max(1e4,`Question must not be longer than 10000 characters`).optional(),m=n.z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/,`Token must be alphanumeric, max 64 chars`),h=n.z.string().regex(/^[A-Za-z0-9_.-]{1,64}$/,`Choice ID must contain only letters, digits, "_", "-" and "." (max 64 chars)`),g=n.z.looseObject({id:h,text:f.max(2e3,`Choice text must not be longer than 2000 characters`),explanation:f.max(1e4,`Choice explanation must not be longer than 10000 characters`).optional()});function _(e,t){return n.z.array(g).min(e,`Must have at least ${e} ${t}`).max(100,`Maximum 100 ${t}`).refine(e=>new Set(e.map(e=>e.id)).size===e.length,{message:`Choice IDs must be unique within the list`})}var v=e=>new Set(e.map(e=>e.id)),ee=n.z.string().max(128,`Skill ID must not be longer than 128 characters`).regex(/^[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*$/,`Skill ID must be lowercase segments joined by dots (e.g. "chess.tactics.fork")`),y=n.z.looseObject({lang:i,value:n.z.string().min(1,`Subtitle URI must not be empty`).regex(/\.(vtt|srt)(\?.*)?$/i,`Subtitle URI must end with .vtt or .srt`),label:n.z.string().optional(),kind:n.z.enum([`captions`,`subtitles`,`descriptions`]).optional()}),b=n.z.looseObject({type:n.z.enum([`image`,`audio`,`video`,`model`]),value:n.z.string().min(1,`Media URI must not be empty`),mimeType:n.z.string().optional(),altText:n.z.string().optional(),transcript:f.optional(),caption:f.optional(),width:n.z.number().int().positive().optional(),height:n.z.number().int().positive().optional(),start:n.z.number().nonnegative().optional(),end:n.z.number().positive().optional(),loop:n.z.boolean().optional(),subtitles:n.z.array(y).optional(),license:a.optional(),attribution:n.z.string().optional(),checksums:n.z.record(n.z.string(),n.z.string()).optional()}).refine(e=>e.start!==void 0&&e.end!==void 0?e.start<e.end:!0,{message:`Start time must be less than end time`,path:[`end`]}),x=n.z.record(l,b),S=n.z.looseObject({name:u,role:n.z.string().optional(),email:n.z.string().email(`Invalid email format`).optional(),url:c.optional()}),C=n.z.enum([`url`,`doi`,`isbn`,`pdf`,`textbook`,`video`,`audio`,`image`,`model`,`other`]),w=n.z.looseObject({id:u,type:C,value:u,title:u,description:n.z.string().optional(),authors:n.z.array(u).optional(),publishedDate:o.optional(),retrievedAt:o.optional(),license:a.optional()}).refine(e=>{if([`url`,`pdf`,`video`,`audio`,`image`].includes(e.type))try{return new URL(e.value),!0}catch{return!1}return!0},{message:`For url, pdf, video, audio and image types, value must be a valid URL`,path:[`value`]}),T=n.z.looseObject({id:u,location:n.z.string().optional(),quote:f.optional()}),te=n.z.looseObject({wikidataId:n.z.string().regex(/^Q\d+$/,`Wikidata ID must have format Q followed by number`).optional(),description:n.z.string().optional()}),ne=n.z.record(n.z.string(),te),re=n.z.string().regex(/^(x-)?[a-z0-9]+(-[a-z0-9]+)*$/,`Feature must be lowercase words joined by hyphens (e.g. "latex", "x-my-feature")`),E=n.z.string().regex(/^(x-)?[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/,`Property must be a key such as "hints" or "x-my-property"`),D=n.z.looseObject({features:n.z.array(re).optional(),latexPackages:n.z.array(n.z.string().min(1,`Package name must not be empty`)).optional(),itemProperties:n.z.array(E).optional(),metaProperties:n.z.array(E).optional()}),O=n.z.looseObject({lang:i,id:r,title:u,downloadUrl:c.optional()}),k=n.z.looseObject({id:r,title:u,downloadUrl:c.optional()}),A=n.z.enum([`remember`,`understand`,`apply`,`analyze`,`evaluate`,`create`]),j=n.z.enum([`low`,`medium`,`high`]),M=n.z.looseObject({bloomLevel:A.optional(),irtDifficulty:n.z.number().optional(),irtDiscrimination:n.z.number().optional(),irtGuessing:n.z.number().min(0).max(1,`IRT guessing must be between 0 and 1`).optional(),avgTime:n.z.number().positive().optional(),cognitiveLoad:j.optional(),partialCredit:n.z.boolean().optional(),penaltyPerWrong:n.z.number().min(0).max(1,`Penalty must be between 0 and 1`).optional()}),N=n.z.looseObject({id:r,language:i,title:u.max(500,`Title must not be longer than 500 characters`),description:f.max(5e3,`Description must not be longer than 5000 characters`).optional(),thumbnail:l.optional(),assets:x.optional(),ageMin:n.z.number().int().nonnegative().optional(),ageMax:n.z.number().int().nonnegative().optional(),subject:n.z.string().optional(),createdAt:o,updatedAt:o,author:S.optional(),contributors:n.z.array(S).optional(),license:a.optional(),licenseUrl:c.optional(),requirements:D.optional(),tags:n.z.array(u).optional(),tagDefinitions:ne.optional(),translations:n.z.array(O).optional(),sourceMaterials:n.z.array(w).optional(),estimatedTime:n.z.number().positive().optional(),prerequisites:n.z.array(k).optional(),relatedSets:n.z.array(k).optional(),customData:n.z.record(n.z.string(),n.z.unknown()).optional(),appSpecific:n.z.record(n.z.string(),n.z.unknown()).optional()}).refine(e=>e.ageMin!==void 0&&e.ageMax!==void 0?e.ageMin<=e.ageMax:!0,{message:`Minimum age (ageMin) must be less than or equal to maximum age (ageMax)`,path:[`ageMax`]}).refine(e=>new Date(e.createdAt)<=new Date(e.updatedAt),{message:`Creation date (createdAt) must be before or equal to update date (updatedAt)`,path:[`updatedAt`]}),P=n.z.looseObject({id:r,type:n.z.string(),assets:x.optional(),lang:i.optional(),tags:n.z.array(u).optional(),skills:n.z.array(ee).max(20,`Maximum 20 skills per item`).optional(),topic:u.optional(),difficulty:n.z.number().int().min(1,`Difficulty must be at least 1`).max(5,`Difficulty must be at most 5`).optional(),timeLimit:n.z.number().positive(`Time limit must be a positive number`).optional(),hints:n.z.array(f.max(2e3,`Hint must not be longer than 2000 characters`)).max(20,`Maximum 20 hints per item`).optional(),explanation:f.max(1e4,`Explanation must not be longer than 10000 characters`).optional(),incorrectFeedback:f.optional(),sources:n.z.array(T).optional(),relatedItems:n.z.array(r).optional(),dependencyItems:n.z.array(r).optional(),pedagogy:M.optional(),customData:n.z.record(n.z.string(),n.z.unknown()).optional(),appSpecific:n.z.record(n.z.string(),n.z.unknown()).optional()}),F=n.z.looseObject({options:_(1,`options`),correctId:h,shuffle:n.z.boolean().optional()}).refine(e=>v(e.options).has(e.correctId),{message:`correctId references a non-existent option`,path:[`correctId`]}),I=n.z.looseObject({type:n.z.literal(`rect`),label:n.z.string().optional(),x:n.z.number().min(0).max(100,`X must be in range 0-100%`),y:n.z.number().min(0).max(100,`Y must be in range 0-100%`),width:n.z.number().min(0).max(100,`Width must be in range 0-100%`),height:n.z.number().min(0).max(100,`Height must be in range 0-100%`)}),L=n.z.looseObject({type:n.z.literal(`circle`),label:n.z.string().optional(),x:n.z.number().min(0).max(100,`X must be in range 0-100%`),y:n.z.number().min(0).max(100,`Y must be in range 0-100%`),radius:n.z.number().min(0).max(100,`Radius must be in range 0-100%`)}),R=n.z.looseObject({type:n.z.literal(`polygon`),label:n.z.string().optional(),points:n.z.array(n.z.looseObject({x:n.z.number().min(0).max(100),y:n.z.number().min(0).max(100)})).min(3,`Polygon must have at least 3 points`)}),z=n.z.looseObject({type:n.z.literal(`mesh`),label:n.z.string().optional(),targetName:u}),ie=n.z.discriminatedUnion(`type`,[I,L,R,z]),B=n.z.discriminatedUnion(`type`,[I,L,R]),V=n.z.looseObject({x:n.z.number(),y:n.z.number(),z:n.z.number()}),H=n.z.looseObject({position:V.optional(),target:V.optional()}),U=n.z.looseObject({id:h,text:f,correctCategoryId:h}),W=n.z.enum([`year`,`month`,`day`,`datetime`]),G=n.z.looseObject({id:u,text:f,date:s,precision:W.optional()}),K=n.z.union([I.extend({id:h,correctLabelId:h}),L.extend({id:h,correctLabelId:h}),R.extend({id:h,correctLabelId:h})]),q=n.z.looseObject({label:u,percentage:n.z.number().min(0,`Percentage must be non-negative`).max(100,`Percentage must not exceed 100`),description:n.z.string().optional()}),J=n.z.looseObject({criteria:n.z.array(q)}).refine(e=>{let t=e.criteria.reduce((e,t)=>e+t.percentage,0);return e.criteria.length===0||t>0},{message:`Sum of percentages of all criteria must be greater than 0`,path:[`criteria`]}),Y=P.extend({type:n.z.literal(`note`),title:n.z.string().optional(),content:f.max(1e5,`Content must not be longer than 100000 characters`),hiddenContent:f.max(1e5,`Hidden content must not be longer than 100000 characters`).optional()}),ae=P.extend({type:n.z.literal(`flashcard`),front:f.max(1e4,`Front side must not be longer than 10000 characters`),back:f.max(1e4,`Back side must not be longer than 10000 characters`)}),oe=P.extend({type:n.z.literal(`true-false`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswer:n.z.boolean()}),se=P.extend({type:n.z.literal(`mcq-single`),question:f.max(1e4,`Question must not be longer than 10000 characters`),options:_(2,`options`),correctId:h,shuffle:n.z.boolean().optional()}).refine(e=>v(e.options).has(e.correctId),{message:`correctId references a non-existent option`,path:[`correctId`]}).refine(e=>new Set(e.options.map(e=>e.text.toLowerCase())).size===e.options.length,{message:`Options must not contain duplicates (case-insensitive)`,path:[`options`]}),ce=P.extend({type:n.z.literal(`mcq-multi`),question:f.max(1e4,`Question must not be longer than 10000 characters`),options:_(2,`options`),correctIds:n.z.array(h).min(1,`Must have at least 1 correct answer`),minSelections:n.z.number().int().positive().optional(),maxSelections:n.z.number().int().positive().optional(),shuffle:n.z.boolean().optional()}).refine(e=>e.correctIds.every(t=>v(e.options).has(t)),{message:`Some of correctIds reference a non-existent option`,path:[`correctIds`]}).refine(e=>new Set(e.correctIds).size===e.correctIds.length,{message:`correctIds contain duplicates`,path:[`correctIds`]}).refine(e=>!(e.minSelections&&e.maxSelections)||e.minSelections<=e.maxSelections,{message:`Minimum number of selections must be less than or equal to maximum`,path:[`maxSelections`]}).refine(e=>!e.maxSelections||e.maxSelections<=e.options.length,{message:`Maximum number of selections must not exceed number of options`,path:[`maxSelections`]}).refine(e=>!e.minSelections||e.minSelections<=e.options.length,{message:`minSelections cannot be greater than the number of options`,path:[`minSelections`]}).refine(e=>new Set(e.options.map(e=>e.text.toLowerCase())).size===e.options.length,{message:`Options must not contain duplicates (case-insensitive)`,path:[`options`]}),le=P.extend({type:n.z.literal(`short-answer`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswers:n.z.array(u).min(1,`Must have at least 1 correct answer`),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional()}),ue=P.extend({type:n.z.literal(`fill-in-blanks`),question:p,text:f.max(1e4,`Text must not be longer than 10000 characters`),blanks:n.z.record(m,n.z.array(u).min(1,`Each blank must have at least 1 correct answer`)),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional()}).refine(e=>Object.keys(e.blanks).length>0,{message:`Text must contain at least 1 blank to fill`,path:[`blanks`]}).refine(e=>{let t=/<blank:([a-zA-Z0-9_-]{1,64})\s*\/>/g,n=new Set,r;for(;(r=t.exec(e.text))!==null;)r[1]&&n.add(r[1]);let i=new Set(Object.keys(e.blanks));for(let e of n)if(!i.has(e))return!1;for(let e of i)if(!n.has(e))return!1;return!0},{message:`All tokens in text must have definition in blanks and all definitions must be used in text`,path:[`blanks`]}),de=P.extend({type:n.z.literal(`fill-in-select`),question:p,text:f.max(1e4,`Text must not be longer than 10000 characters`),blanks:n.z.record(m,F)}).refine(e=>Object.keys(e.blanks).length>0,{message:`Text must contain at least 1 blank to select`,path:[`blanks`]}).refine(e=>{let t=/<blank:([a-zA-Z0-9_-]{1,64})\s*\/>/g,n=new Set,r;for(;(r=t.exec(e.text))!==null;)r[1]&&n.add(r[1]);let i=new Set(Object.keys(e.blanks));for(let e of n)if(!i.has(e))return!1;for(let e of i)if(!n.has(e))return!1;return!0},{message:`All tokens in text must have definition in blanks and all definitions must be used in text`,path:[`blanks`]}),fe=P.extend({type:n.z.literal(`match-pairs`),question:p,prompts:_(2,`prompts`),matches:_(2,`matches`),pairs:n.z.record(h,h)}).refine(e=>e.prompts.every(t=>t.id in e.pairs)&&Object.keys(e.pairs).every(t=>v(e.prompts).has(t)),{message:`pairs must have exactly one entry for every prompt`,path:[`pairs`]}).refine(e=>Object.values(e.pairs).every(t=>v(e.matches).has(t)),{message:`Some pair references a non-existent match`,path:[`pairs`]}).refine(e=>new Set(Object.values(e.pairs)).size===Object.values(e.pairs).length,{message:`Each match may belong to only one prompt`,path:[`pairs`]}),pe=P.extend({type:n.z.literal(`match-complex`),question:p,leftItems:_(1,`items on the left`),rightItems:_(1,`items on the right`),connections:n.z.array(n.z.tuple([h,h])).min(1,`Must have at least 1 connection`),minCorrect:n.z.number().int().positive().optional()}).refine(e=>e.connections.every(([t,n])=>v(e.leftItems).has(t)&&v(e.rightItems).has(n)),{message:`Some connection references a non-existent item`,path:[`connections`]}).refine(e=>new Set(e.connections.map(([e,t])=>`${e}\u0000${t}`)).size===e.connections.length,{message:`Connections contain duplicates`,path:[`connections`]}).refine(e=>!e.minCorrect||e.minCorrect<=e.connections.length,{message:`Minimum correct answers must not exceed total number of connections`,path:[`minCorrect`]}),me=P.extend({type:n.z.literal(`sort-items`),question:f.max(1e4,`Question must not be longer than 10000 characters`),items:_(2,`items to sort`),correctOrder:n.z.array(h)}).refine(e=>e.correctOrder.length===e.items.length&&new Set(e.correctOrder).size===e.items.length&&e.correctOrder.every(t=>v(e.items).has(t)),{message:`correctOrder must list every item ID exactly once`,path:[`correctOrder`]}),he=P.extend({type:n.z.literal(`slider`),question:f.max(1e4,`Question must not be longer than 10000 characters`),min:n.z.number(),max:n.z.number(),step:n.z.number().positive(`Step must be a positive number`),correctAnswer:n.z.number(),tolerance:n.z.number().nonnegative(`Tolerance must be non-negative`).optional(),unit:n.z.string().optional()}).refine(e=>e.min<e.max,{message:`Minimum value must be less than maximum value`,path:[`max`]}).refine(e=>e.correctAnswer>=e.min&&e.correctAnswer<=e.max,{message:`Correct answer must be in range min-max`,path:[`correctAnswer`]}).refine(e=>{let t=(e.correctAnswer-e.min)/e.step;return Math.abs(t-Math.round(t))<1e-4},{message:`Correct answer must be reachable by defined step`,path:[`correctAnswer`]}).refine(e=>(e.tolerance??0)<=(e.max-e.min)/2,{message:`Tolerance must not be greater than half of value range`,path:[`tolerance`]}),ge=P.extend({type:n.z.literal(`pin-on-image`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,hotspots:n.z.array(B).min(1,`Must have at least 1 hotspot`),multipleCorrect:n.z.boolean().optional(),minCorrect:n.z.number().int().positive().optional()}).refine(e=>!e.minCorrect||e.minCorrect<=e.hotspots.length,{message:`Minimum correct answers must not exceed number of hotspots`,path:[`minCorrect`]}),_e=P.extend({type:n.z.literal(`categorize`),question:f.max(1e4,`Question must not be longer than 10000 characters`),categories:_(2,`categories`),items:n.z.array(U).min(1,`Must have at least 1 item to categorize`)}).refine(e=>new Set(e.categories.map(e=>e.text.toLowerCase())).size===e.categories.length,{message:`Categories must not contain duplicates (case-insensitive)`,path:[`categories`]}).refine(e=>e.items.every(t=>v(e.categories).has(t.correctCategoryId)),{message:`Some item references a non-existent category`,path:[`items`]}),ve=P.extend({type:n.z.literal(`timeline`),question:f.max(1e4,`Question must not be longer than 10000 characters`),events:n.z.array(G).min(2,`Must have at least 2 events`),shuffle:n.z.boolean().optional()}),ye=P.extend({type:n.z.literal(`matrix`),question:f.max(1e4,`Question must not be longer than 10000 characters`),rows:_(1,`rows`),columns:_(1,`columns`),correctCells:n.z.array(n.z.tuple([h,h])).min(1,`Must have at least 1 correct cell`),multiplePerRow:n.z.boolean().optional()}).refine(e=>e.correctCells.every(([t,n])=>v(e.rows).has(t)&&v(e.columns).has(n)),{message:`Some cell references a non-existent row or column`,path:[`correctCells`]}).refine(e=>new Set(e.correctCells.map(([e,t])=>`${e}\u0000${t}`)).size===e.correctCells.length,{message:`Correct cells contain duplicates`,path:[`correctCells`]}).refine(e=>e.multiplePerRow||new Set(e.correctCells.map(([e])=>e)).size===e.correctCells.length,{message:`If multiplePerRow is false, each row may have maximum 1 correct cell`,path:[`correctCells`]}),be=P.extend({type:n.z.literal(`math-input`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswer:d,alternativeAnswers:n.z.array(d).optional(),tolerance:n.z.number().nonnegative().optional()}),xe=P.extend({type:n.z.literal(`diagram-label`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,labels:_(1,`labels`),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional(),requireTyping:n.z.boolean().optional(),zones:n.z.array(K).min(1,`Must have at least 1 zone`)}).refine(e=>e.zones.every(t=>v(e.labels).has(t.correctLabelId)),{message:`Some zone references a non-existent label`,path:[`zones`]}).refine(e=>new Set(e.zones.map(e=>e.id)).size===e.zones.length,{message:`Zone IDs must be unique within the item`,path:[`zones`]}),Se=P.extend({type:n.z.literal(`open-ended`),question:f.max(1e4,`Question must not be longer than 10000 characters`),minWords:n.z.number().int().positive().optional(),maxWords:n.z.number().int().positive().optional(),sampleAnswer:f.optional(),rubric:J.optional()}).refine(e=>e.minWords&&e.maxWords?e.minWords<=e.maxWords:!0,{message:`Minimum word count must be less than or equal to maximum`,path:[`maxWords`]}),Ce=P.extend({type:n.z.literal(`numeric-input`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswer:n.z.number(),tolerance:n.z.number().nonnegative(`Tolerance must be non-negative`).optional(),unit:n.z.string().optional()}),X=P.extend({type:n.z.literal(`pin-on-model`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,hotspots:n.z.array(z).min(1,`Must have at least 1 hotspot`),multipleCorrect:n.z.boolean().optional(),minCorrect:n.z.number().int().positive().optional(),camera:H.optional()}).refine(e=>!e.minCorrect||e.minCorrect<=e.hotspots.length,{message:`Minimum correct answers must not exceed number of hotspots`,path:[`minCorrect`]}),we=P.extend({type:n.z.literal(`chess-puzzle`),question:f.max(1e4,`Question must not be longer than 10000 characters`),fen:u,correctAnswers:n.z.array(n.z.array(u).min(1,`Move sequence must not be empty`)).min(1,`Must have at least 1 correct move sequence`),elo:n.z.number().int().nonnegative(`ELO must be non-negative integer`).optional()}),Z=n.z.discriminatedUnion(`type`,[Y,ae,oe,se,ce,le,ue,de,fe,pe,me,he,ge,_e,ve,ye,be,xe,Se,Ce,X,we]),Te=n.z.string().regex(/^x-[a-z0-9]+(?:-[a-z0-9]+)*$/,`Custom item type must start with "x-" followed by lowercase letters, digits and hyphens`),Q=P.extend({type:Te}),Ee=n.z.union([Z,Q]);function $(e){let t=e?.type;return typeof t==`string`&&t.startsWith(`x-`)?Q:Z}var De=n.z.looseObject({$schema:n.z.string().url().optional(),version:n.z.string().regex(/^\d+\.\d+$/,`Version must be in MAJOR.MINOR format (e.g. "1.0")`),meta:N,items:n.z.array(Ee).max(1e4,`Maximum 10000 items per set`)});function Oe(e){let t=e instanceof Error?e.message:`OQSE Security Error: Maximum nesting depth exceeded limit of 10 levels.`;return new n.z.ZodError([{code:`custom`,message:t,path:[],input:void 0}])}function ke(t){return e.validateJsonDepth(t,10),$(t).parse(t)}function Ae(t){try{e.validateJsonDepth(t,10)}catch(e){return{success:!1,error:Oe(e)}}let n=$(t).safeParse(t);return n.success?{success:!0,data:n.data}:{success:!1,error:n.error}}exports.AbsoluteURLSchema=c,exports.AnyOQSEItemSchema=Ee,exports.AssetDictionarySchema=x,exports.AssetKeySchema=l,exports.BaseItemSchema=P,exports.BlankTokenSchema=m,exports.BloomLevelSchema=A,exports.CameraSetupSchema=H,exports.CategorizeEntrySchema=U,exports.CategorizeItemSchema=_e,exports.ChessPuzzleItemSchema=we,exports.ChoiceIdSchema=h,exports.ChoiceSchema=g,exports.CircleHotspotSchema=L,exports.CognitiveLoadSchema=j,exports.CustomItemSchema=Q,exports.CustomItemTypeSchema=Te,exports.DiagramLabelItemSchema=xe,exports.DiagramZoneSchema=K,exports.FeatureKeySchema=re,exports.FeatureProfileSchema=D,exports.FillInBlanksItemSchema=ue,exports.FillInSelectItemSchema=de,exports.FlashcardItemSchema=ae,exports.Hotspot2DSchema=B,exports.HotspotObjectSchema=ie,exports.ISO8601DateTimeSchema=o,exports.LanguageCodeSchema=i,exports.LinkedSetObjectSchema=k,exports.MCQMultiItemSchema=ce,exports.MCQSingleItemSchema=se,exports.MatchComplexItemSchema=pe,exports.MatchPairsItemSchema=fe,exports.MathInputItemSchema=be,exports.MatrixItemSchema=ye,exports.MediaObjectSchema=b,exports.MeshHotspotSchema=z,exports.NoteItemSchema=Y,exports.NumericInputItemSchema=Ce,exports.OQSEFileSchema=De,exports.OQSEItemSchema=Z,exports.OQSEMetaSchema=N,exports.OpenEndedItemSchema=Se,exports.OptionalRichContentSchema=p,exports.PedagogySchema=M,exports.PersonObjectSchema=S,exports.PinOnImageItemSchema=ge,exports.PinOnModelItemSchema=X,exports.PlainTextSchema=u,exports.PolygonHotspotSchema=R,exports.PropertyKeySchema=E,exports.RawLatexSchema=d,exports.RectHotspotSchema=I,exports.RichContentSchema=f,exports.RubricCriterionSchema=q,exports.RubricSchema=J,exports.SPDXLicenseSchema=a,exports.SelectBlankObjectSchema=F,exports.ShortAnswerItemSchema=le,exports.SkillIdSchema=ee,exports.SliderItemSchema=he,exports.SortItemsItemSchema=me,exports.SourceMaterialSchema=w,exports.SourceMaterialTypeSchema=C,exports.SourceReferenceSchema=T,exports.SubtitleTrackSchema=y,exports.TagDefinitionDictionarySchema=ne,exports.TagDefinitionSchema=te,exports.TimelineDateSchema=s,exports.TimelineEventSchema=G,exports.TimelineItemSchema=ve,exports.TimelinePrecisionSchema=W,exports.TranslationObjectSchema=O,exports.TrueFalseItemSchema=oe,exports.UUIDSchema=r,exports.Vector3Schema=V,exports.choiceList=_,exports.getItemSchema=$,exports.safeValidateOQSEItem=Ae,exports.validateOQSEItem=ke;
|
|
1
|
+
const e=require("./utils.cjs"),t=require("./answers.cjs");let n=require("zod");var r=n.z.string().uuid({message:`Invalid UUID format`}),i=n.z.string().min(2,`Language code must have at least 2 characters`).regex(/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/,`Invalid BCP 47 format (e.g. "cs", "en-US")`),a=n.z.string().min(1,`License identifier must not be empty`),o=n.z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/,`Invalid ISO 8601 format (e.g. "2025-11-21T14:30:00Z")`),s=n.z.string().regex(/^-?\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/,`Invalid date (e.g. "1969-07-20", or "-0480-08-01" for 480 BC)`).refine(t.isTimelineDate,`Year 0000 does not exist (1 BC is "-0001", AD 1 is "0001")`),c=n.z.string().url({message:`Must be a valid absolute URL`}),l=n.z.string().regex(/^[a-z0-9_-]+$/,`Asset key must contain only lowercase letters, numbers, hyphens and underscores`),u=n.z.string().refine(e=>e.trim().length>0,`Text must not be empty`).refine(e=>!/<asset:[^>]*>/i.test(e),`Plain Text fields must not contain media tags (<asset:key />)`),d=u.refine(e=>!/^\s*\$|\$\s*$/.test(e),`LaTeX answer must not be wrapped in $ delimiters (write "2x + 2", not "$2x + 2$")`),f=n.z.string().min(1,`Content must not be empty`),p=n.z.string().max(1e4,`Question must not be longer than 10000 characters`).optional(),m=n.z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/,`Token must be alphanumeric, max 64 chars`),h=n.z.string().regex(/^[A-Za-z0-9_.-]{1,64}$/,`Choice ID must contain only letters, digits, "_", "-" and "." (max 64 chars)`),g=n.z.looseObject({id:h,text:f.max(2e3,`Choice text must not be longer than 2000 characters`),explanation:f.max(1e4,`Choice explanation must not be longer than 10000 characters`).optional()});function _(e,t){return n.z.array(g).min(e,`Must have at least ${e} ${t}`).max(100,`Maximum 100 ${t}`).refine(e=>new Set(e.map(e=>e.id)).size===e.length,{message:`Choice IDs must be unique within the list`})}var v=e=>new Set(e.map(e=>e.id)),y=n.z.string().max(128,`Skill ID must not be longer than 128 characters`).regex(/^[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*$/,`Skill ID must be lowercase segments joined by dots (e.g. "chess.tactics.fork")`),b=n.z.looseObject({lang:i,value:n.z.string().min(1,`Subtitle URI must not be empty`).regex(/\.(vtt|srt)(\?.*)?$/i,`Subtitle URI must end with .vtt or .srt`),label:n.z.string().optional(),kind:n.z.enum([`captions`,`subtitles`,`descriptions`]).optional()}),x=n.z.looseObject({type:n.z.enum([`image`,`audio`,`video`,`model`]),value:n.z.string().min(1,`Media URI must not be empty`),mimeType:n.z.string().optional(),altText:n.z.string().optional(),transcript:f.optional(),caption:f.optional(),width:n.z.number().int().positive().optional(),height:n.z.number().int().positive().optional(),start:n.z.number().nonnegative().optional(),end:n.z.number().positive().optional(),loop:n.z.boolean().optional(),subtitles:n.z.array(b).optional(),license:a.optional(),attribution:n.z.string().optional(),checksums:n.z.record(n.z.string(),n.z.string()).optional()}).refine(e=>e.start!==void 0&&e.end!==void 0?e.start<e.end:!0,{message:`Start time must be less than end time`,path:[`end`]}),S=n.z.record(l,x),C=n.z.looseObject({name:u,role:n.z.string().optional(),email:n.z.string().email(`Invalid email format`).optional(),url:c.optional()}),w=n.z.enum([`url`,`doi`,`isbn`,`pdf`,`textbook`,`video`,`audio`,`image`,`model`,`other`]),T=n.z.looseObject({id:u,type:w,value:u,title:u,description:n.z.string().optional(),authors:n.z.array(u).optional(),publishedDate:o.optional(),retrievedAt:o.optional(),license:a.optional()}).refine(e=>{if([`url`,`pdf`,`video`,`audio`,`image`].includes(e.type))try{return new URL(e.value),!0}catch{return!1}return!0},{message:`For url, pdf, video, audio and image types, value must be a valid URL`,path:[`value`]}),ee=n.z.looseObject({id:u,location:n.z.string().optional(),quote:f.optional()}),te=n.z.looseObject({wikidataId:n.z.string().regex(/^Q\d+$/,`Wikidata ID must have format Q followed by number`).optional(),description:n.z.string().optional()}),ne=n.z.record(n.z.string(),te),re=n.z.string().regex(/^(x-)?[a-z0-9]+(-[a-z0-9]+)*$/,`Feature must be lowercase words joined by hyphens (e.g. "latex", "x-my-feature")`),E=n.z.string().regex(/^(x-)?[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/,`Property must be a key such as "hints" or "x-my-property"`),D=n.z.looseObject({features:n.z.array(re).optional(),latexPackages:n.z.array(n.z.string().min(1,`Package name must not be empty`)).optional(),itemProperties:n.z.array(E).optional(),metaProperties:n.z.array(E).optional()}),O=n.z.looseObject({lang:i,id:r,title:u,downloadUrl:c.optional()}),k=n.z.looseObject({id:r,title:u,downloadUrl:c.optional()}),A=n.z.enum([`remember`,`understand`,`apply`,`analyze`,`evaluate`,`create`]),j=n.z.enum([`low`,`medium`,`high`]),M=n.z.looseObject({bloomLevel:A.optional(),irtDifficulty:n.z.number().optional(),irtDiscrimination:n.z.number().optional(),irtGuessing:n.z.number().min(0).max(1,`IRT guessing must be between 0 and 1`).optional(),avgTime:n.z.number().positive().optional(),cognitiveLoad:j.optional(),partialCredit:n.z.boolean().optional(),penaltyPerWrong:n.z.number().min(0).max(1,`Penalty must be between 0 and 1`).optional()}),N=n.z.looseObject({id:r,language:i,title:u.max(500,`Title must not be longer than 500 characters`),description:f.max(5e3,`Description must not be longer than 5000 characters`).optional(),thumbnail:l.optional(),assets:S.optional(),ageMin:n.z.number().int().nonnegative().optional(),ageMax:n.z.number().int().nonnegative().optional(),subject:n.z.string().optional(),createdAt:o,updatedAt:o,author:C.optional(),contributors:n.z.array(C).optional(),license:a.optional(),licenseUrl:c.optional(),requirements:D.optional(),tags:n.z.array(u).optional(),tagDefinitions:ne.optional(),translations:n.z.array(O).optional(),sourceMaterials:n.z.array(T).optional(),estimatedTime:n.z.number().positive().optional(),prerequisites:n.z.array(k).optional(),relatedSets:n.z.array(k).optional(),customData:n.z.record(n.z.string(),n.z.unknown()).optional(),appSpecific:n.z.record(n.z.string(),n.z.unknown()).optional()}).refine(e=>e.ageMin!==void 0&&e.ageMax!==void 0?e.ageMin<=e.ageMax:!0,{message:`Minimum age (ageMin) must be less than or equal to maximum age (ageMax)`,path:[`ageMax`]}).refine(e=>new Date(e.createdAt)<=new Date(e.updatedAt),{message:`Creation date (createdAt) must be before or equal to update date (updatedAt)`,path:[`updatedAt`]}),P=n.z.looseObject({id:r,type:n.z.string(),assets:S.optional(),lang:i.optional(),tags:n.z.array(u).optional(),skills:n.z.array(y).max(20,`Maximum 20 skills per item`).optional(),topic:u.optional(),difficulty:n.z.number().int().min(1,`Difficulty must be at least 1`).max(5,`Difficulty must be at most 5`).optional(),timeLimit:n.z.number().positive(`Time limit must be a positive number`).optional(),hints:n.z.array(f.max(2e3,`Hint must not be longer than 2000 characters`)).max(20,`Maximum 20 hints per item`).optional(),explanation:f.max(1e4,`Explanation must not be longer than 10000 characters`).optional(),incorrectFeedback:f.optional(),sources:n.z.array(ee).optional(),relatedItems:n.z.array(r).optional(),dependencyItems:n.z.array(r).optional(),pedagogy:M.optional(),customData:n.z.record(n.z.string(),n.z.unknown()).optional(),appSpecific:n.z.record(n.z.string(),n.z.unknown()).optional()}),F=n.z.looseObject({options:_(1,`options`),correctId:h,shuffle:n.z.boolean().optional()}).refine(e=>v(e.options).has(e.correctId),{message:`correctId references a non-existent option`,path:[`correctId`]}),I=n.z.looseObject({type:n.z.literal(`rect`),label:n.z.string().optional(),x:n.z.number().min(0).max(100,`X must be in range 0-100%`),y:n.z.number().min(0).max(100,`Y must be in range 0-100%`),width:n.z.number().min(0).max(100,`Width must be in range 0-100%`),height:n.z.number().min(0).max(100,`Height must be in range 0-100%`)}),L=n.z.looseObject({type:n.z.literal(`circle`),label:n.z.string().optional(),x:n.z.number().min(0).max(100,`X must be in range 0-100%`),y:n.z.number().min(0).max(100,`Y must be in range 0-100%`),radius:n.z.number().min(0).max(100,`Radius must be in range 0-100%`)}),R=n.z.looseObject({type:n.z.literal(`polygon`),label:n.z.string().optional(),points:n.z.array(n.z.looseObject({x:n.z.number().min(0).max(100),y:n.z.number().min(0).max(100)})).min(3,`Polygon must have at least 3 points`)}),z=n.z.looseObject({type:n.z.literal(`mesh`),label:n.z.string().optional(),targetName:u}),ie=n.z.discriminatedUnion(`type`,[I,L,R,z]),B=n.z.discriminatedUnion(`type`,[I,L,R]),V=n.z.looseObject({x:n.z.number(),y:n.z.number(),z:n.z.number()}),H=n.z.looseObject({position:V.optional(),target:V.optional()}),U=n.z.looseObject({id:h,text:f,correctCategoryId:h}),W=n.z.enum([`year`,`month`,`day`,`datetime`]),G=n.z.looseObject({id:u,text:f,date:s,precision:W.optional()}),K=n.z.union([I.extend({id:h,correctLabelId:h}),L.extend({id:h,correctLabelId:h}),R.extend({id:h,correctLabelId:h})]),q=n.z.looseObject({label:u,percentage:n.z.number().min(0,`Percentage must be non-negative`).max(100,`Percentage must not exceed 100`),description:n.z.string().optional()}),J=n.z.looseObject({criteria:n.z.array(q)}).refine(e=>{let t=e.criteria.reduce((e,t)=>e+t.percentage,0);return e.criteria.length===0||t>0},{message:`Sum of percentages of all criteria must be greater than 0`,path:[`criteria`]}),ae=P.extend({type:n.z.literal(`note`),title:n.z.string().optional(),content:f.max(1e5,`Content must not be longer than 100000 characters`),hiddenContent:f.max(1e5,`Hidden content must not be longer than 100000 characters`).optional()}),Y=P.extend({type:n.z.literal(`flashcard`),front:f.max(1e4,`Front side must not be longer than 10000 characters`),back:f.max(1e4,`Back side must not be longer than 10000 characters`)}),oe=P.extend({type:n.z.literal(`true-false`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswer:n.z.boolean()}),se=P.extend({type:n.z.literal(`mcq-single`),question:f.max(1e4,`Question must not be longer than 10000 characters`),options:_(2,`options`),correctId:h,shuffle:n.z.boolean().optional()}).refine(e=>v(e.options).has(e.correctId),{message:`correctId references a non-existent option`,path:[`correctId`]}).refine(e=>new Set(e.options.map(e=>e.text.toLowerCase())).size===e.options.length,{message:`Options must not contain duplicates (case-insensitive)`,path:[`options`]}),ce=P.extend({type:n.z.literal(`mcq-multi`),question:f.max(1e4,`Question must not be longer than 10000 characters`),options:_(2,`options`),correctIds:n.z.array(h).min(1,`Must have at least 1 correct answer`),minSelections:n.z.number().int().positive().optional(),maxSelections:n.z.number().int().positive().optional(),shuffle:n.z.boolean().optional()}).refine(e=>e.correctIds.every(t=>v(e.options).has(t)),{message:`Some of correctIds reference a non-existent option`,path:[`correctIds`]}).refine(e=>new Set(e.correctIds).size===e.correctIds.length,{message:`correctIds contain duplicates`,path:[`correctIds`]}).refine(e=>!(e.minSelections&&e.maxSelections)||e.minSelections<=e.maxSelections,{message:`Minimum number of selections must be less than or equal to maximum`,path:[`maxSelections`]}).refine(e=>!e.maxSelections||e.maxSelections<=e.options.length,{message:`Maximum number of selections must not exceed number of options`,path:[`maxSelections`]}).refine(e=>!e.minSelections||e.minSelections<=e.options.length,{message:`minSelections cannot be greater than the number of options`,path:[`minSelections`]}).refine(e=>new Set(e.options.map(e=>e.text.toLowerCase())).size===e.options.length,{message:`Options must not contain duplicates (case-insensitive)`,path:[`options`]}),le=P.extend({type:n.z.literal(`short-answer`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswers:n.z.array(u).min(1,`Must have at least 1 correct answer`),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional()}),ue=P.extend({type:n.z.literal(`fill-in-blanks`),question:p,text:f.max(1e4,`Text must not be longer than 10000 characters`),blanks:n.z.record(m,n.z.array(u).min(1,`Each blank must have at least 1 correct answer`)),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional()}).refine(e=>Object.keys(e.blanks).length>0,{message:`Text must contain at least 1 blank to fill`,path:[`blanks`]}).refine(e=>{let t=/<blank:([a-zA-Z0-9_-]{1,64})\s*\/>/g,n=new Set,r;for(;(r=t.exec(e.text))!==null;)r[1]&&n.add(r[1]);let i=new Set(Object.keys(e.blanks));for(let e of n)if(!i.has(e))return!1;for(let e of i)if(!n.has(e))return!1;return!0},{message:`All tokens in text must have definition in blanks and all definitions must be used in text`,path:[`blanks`]}),de=P.extend({type:n.z.literal(`fill-in-select`),question:p,text:f.max(1e4,`Text must not be longer than 10000 characters`),blanks:n.z.record(m,F)}).refine(e=>Object.keys(e.blanks).length>0,{message:`Text must contain at least 1 blank to select`,path:[`blanks`]}).refine(e=>{let t=/<blank:([a-zA-Z0-9_-]{1,64})\s*\/>/g,n=new Set,r;for(;(r=t.exec(e.text))!==null;)r[1]&&n.add(r[1]);let i=new Set(Object.keys(e.blanks));for(let e of n)if(!i.has(e))return!1;for(let e of i)if(!n.has(e))return!1;return!0},{message:`All tokens in text must have definition in blanks and all definitions must be used in text`,path:[`blanks`]}),fe=P.extend({type:n.z.literal(`match-pairs`),question:p,prompts:_(2,`prompts`),matches:_(2,`matches`),pairs:n.z.record(h,h)}).refine(e=>e.prompts.every(t=>t.id in e.pairs)&&Object.keys(e.pairs).every(t=>v(e.prompts).has(t)),{message:`pairs must have exactly one entry for every prompt`,path:[`pairs`]}).refine(e=>Object.values(e.pairs).every(t=>v(e.matches).has(t)),{message:`Some pair references a non-existent match`,path:[`pairs`]}).refine(e=>new Set(Object.values(e.pairs)).size===Object.values(e.pairs).length,{message:`Each match may belong to only one prompt`,path:[`pairs`]}),pe=P.extend({type:n.z.literal(`match-complex`),question:p,prompts:_(1,`prompts`),matches:_(1,`matches`),connections:n.z.array(n.z.tuple([h,h])).min(1,`Must have at least 1 connection`),minCorrect:n.z.number().int().positive().optional()}).refine(e=>e.connections.every(([t,n])=>v(e.prompts).has(t)&&v(e.matches).has(n)),{message:`Some connection references a non-existent item`,path:[`connections`]}).refine(e=>new Set(e.connections.map(([e,t])=>`${e}\u0000${t}`)).size===e.connections.length,{message:`Connections contain duplicates`,path:[`connections`]}).refine(e=>!e.minCorrect||e.minCorrect<=e.connections.length,{message:`Minimum correct answers must not exceed total number of connections`,path:[`minCorrect`]}),me=P.extend({type:n.z.literal(`sort-items`),question:f.max(1e4,`Question must not be longer than 10000 characters`),items:_(2,`items to sort`),correctOrder:n.z.array(h)}).refine(e=>e.correctOrder.length===e.items.length&&new Set(e.correctOrder).size===e.items.length&&e.correctOrder.every(t=>v(e.items).has(t)),{message:`correctOrder must list every item ID exactly once`,path:[`correctOrder`]}),he=P.extend({type:n.z.literal(`slider`),question:f.max(1e4,`Question must not be longer than 10000 characters`),min:n.z.number(),max:n.z.number(),step:n.z.number().positive(`Step must be a positive number`),correctAnswer:n.z.number(),tolerance:n.z.number().nonnegative(`Tolerance must be non-negative`).optional(),unit:n.z.string().optional()}).refine(e=>e.min<e.max,{message:`Minimum value must be less than maximum value`,path:[`max`]}).refine(e=>e.correctAnswer>=e.min&&e.correctAnswer<=e.max,{message:`Correct answer must be in range min-max`,path:[`correctAnswer`]}).refine(e=>{let t=(e.correctAnswer-e.min)/e.step;return Math.abs(t-Math.round(t))<1e-4},{message:`Correct answer must be reachable by defined step`,path:[`correctAnswer`]}).refine(e=>(e.tolerance??0)<=(e.max-e.min)/2,{message:`Tolerance must not be greater than half of value range`,path:[`tolerance`]}),ge=P.extend({type:n.z.literal(`pin-on-image`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,hotspots:n.z.array(B).min(1,`Must have at least 1 hotspot`),multipleCorrect:n.z.boolean().optional(),minCorrect:n.z.number().int().positive().optional()}).refine(e=>!e.minCorrect||e.minCorrect<=e.hotspots.length,{message:`Minimum correct answers must not exceed number of hotspots`,path:[`minCorrect`]}),_e=P.extend({type:n.z.literal(`categorize`),question:f.max(1e4,`Question must not be longer than 10000 characters`),categories:_(2,`categories`),items:n.z.array(U).min(1,`Must have at least 1 item to categorize`)}).refine(e=>new Set(e.categories.map(e=>e.text.toLowerCase())).size===e.categories.length,{message:`Categories must not contain duplicates (case-insensitive)`,path:[`categories`]}).refine(e=>e.items.every(t=>v(e.categories).has(t.correctCategoryId)),{message:`Some item references a non-existent category`,path:[`items`]}),ve=P.extend({type:n.z.literal(`timeline`),question:f.max(1e4,`Question must not be longer than 10000 characters`),events:n.z.array(G).min(2,`Must have at least 2 events`),shuffle:n.z.boolean().optional()}),ye=P.extend({type:n.z.literal(`matrix`),question:f.max(1e4,`Question must not be longer than 10000 characters`),rows:_(1,`rows`),columns:_(1,`columns`),correctCells:n.z.array(n.z.tuple([h,h])).min(1,`Must have at least 1 correct cell`),multiplePerRow:n.z.boolean().optional()}).refine(e=>e.correctCells.every(([t,n])=>v(e.rows).has(t)&&v(e.columns).has(n)),{message:`Some cell references a non-existent row or column`,path:[`correctCells`]}).refine(e=>new Set(e.correctCells.map(([e,t])=>`${e}\u0000${t}`)).size===e.correctCells.length,{message:`Correct cells contain duplicates`,path:[`correctCells`]}).refine(e=>e.multiplePerRow||new Set(e.correctCells.map(([e])=>e)).size===e.correctCells.length,{message:`If multiplePerRow is false, each row may have maximum 1 correct cell`,path:[`correctCells`]}),be=P.extend({type:n.z.literal(`math-input`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswers:n.z.array(d).min(1,`Must have at least 1 correct answer`)}),xe=P.extend({type:n.z.literal(`diagram-label`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,labels:_(1,`labels`),caseSensitive:n.z.boolean().optional(),ignoreDiacritics:n.z.boolean().optional(),acceptPartial:n.z.boolean().optional(),requireTyping:n.z.boolean().optional(),zones:n.z.array(K).min(1,`Must have at least 1 zone`)}).refine(e=>e.zones.every(t=>v(e.labels).has(t.correctLabelId)),{message:`Some zone references a non-existent label`,path:[`zones`]}).refine(e=>new Set(e.zones.map(e=>e.id)).size===e.zones.length,{message:`Zone IDs must be unique within the item`,path:[`zones`]}),Se=P.extend({type:n.z.literal(`open-ended`),question:f.max(1e4,`Question must not be longer than 10000 characters`),minWords:n.z.number().int().positive().optional(),maxWords:n.z.number().int().positive().optional(),sampleAnswer:f.optional(),rubric:J.optional()}).refine(e=>e.minWords&&e.maxWords?e.minWords<=e.maxWords:!0,{message:`Minimum word count must be less than or equal to maximum`,path:[`maxWords`]}),Ce=P.extend({type:n.z.literal(`numeric-input`),question:f.max(1e4,`Question must not be longer than 10000 characters`),correctAnswer:n.z.number(),tolerance:n.z.number().nonnegative(`Tolerance must be non-negative`).optional(),unit:n.z.string().optional()}),X=P.extend({type:n.z.literal(`pin-on-model`),question:f.max(1e4,`Question must not be longer than 10000 characters`),targetAsset:l,hotspots:n.z.array(z).min(1,`Must have at least 1 hotspot`),multipleCorrect:n.z.boolean().optional(),minCorrect:n.z.number().int().positive().optional(),camera:H.optional()}).refine(e=>!e.minCorrect||e.minCorrect<=e.hotspots.length,{message:`Minimum correct answers must not exceed number of hotspots`,path:[`minCorrect`]}),we=P.extend({type:n.z.literal(`chess-puzzle`),question:f.max(1e4,`Question must not be longer than 10000 characters`),fen:u,correctAnswers:n.z.array(n.z.array(u).min(1,`Move sequence must not be empty`)).min(1,`Must have at least 1 correct move sequence`),elo:n.z.number().int().nonnegative(`ELO must be non-negative integer`).optional()}),Z=n.z.discriminatedUnion(`type`,[ae,Y,oe,se,ce,le,ue,de,fe,pe,me,he,ge,_e,ve,ye,be,xe,Se,Ce,X,we]),Te=n.z.string().regex(/^x-[a-z0-9]+(?:-[a-z0-9]+)*$/,`Custom item type must start with "x-" followed by lowercase letters, digits and hyphens`),Q=P.extend({type:Te}),Ee=n.z.union([Z,Q]);function $(e){let t=e?.type;return typeof t==`string`&&t.startsWith(`x-`)?Q:Z}var De=n.z.looseObject({$schema:n.z.string().url().optional(),version:n.z.string().regex(/^\d+\.\d+$/,`Version must be in MAJOR.MINOR format (e.g. "1.0")`),meta:N,items:n.z.array(Ee).max(1e4,`Maximum 10000 items per set`)});function Oe(e){let t=e instanceof Error?e.message:`OQSE Security Error: Maximum nesting depth exceeded limit of 10 levels.`;return new n.z.ZodError([{code:`custom`,message:t,path:[],input:void 0}])}function ke(t){return e.validateJsonDepth(t,10),$(t).parse(t)}function Ae(t){try{e.validateJsonDepth(t,10)}catch(e){return{success:!1,error:Oe(e)}}let n=$(t).safeParse(t);return n.success?{success:!0,data:n.data}:{success:!1,error:n.error}}exports.AbsoluteURLSchema=c,exports.AnyOQSEItemSchema=Ee,exports.AssetDictionarySchema=S,exports.AssetKeySchema=l,exports.BaseItemSchema=P,exports.BlankTokenSchema=m,exports.BloomLevelSchema=A,exports.CameraSetupSchema=H,exports.CategorizeEntrySchema=U,exports.CategorizeItemSchema=_e,exports.ChessPuzzleItemSchema=we,exports.ChoiceIdSchema=h,exports.ChoiceSchema=g,exports.CircleHotspotSchema=L,exports.CognitiveLoadSchema=j,exports.CustomItemSchema=Q,exports.CustomItemTypeSchema=Te,exports.DiagramLabelItemSchema=xe,exports.DiagramZoneSchema=K,exports.FeatureKeySchema=re,exports.FeatureProfileSchema=D,exports.FillInBlanksItemSchema=ue,exports.FillInSelectItemSchema=de,exports.FlashcardItemSchema=Y,exports.Hotspot2DSchema=B,exports.HotspotObjectSchema=ie,exports.ISO8601DateTimeSchema=o,exports.LanguageCodeSchema=i,exports.LinkedSetObjectSchema=k,exports.MCQMultiItemSchema=ce,exports.MCQSingleItemSchema=se,exports.MatchComplexItemSchema=pe,exports.MatchPairsItemSchema=fe,exports.MathInputItemSchema=be,exports.MatrixItemSchema=ye,exports.MediaObjectSchema=x,exports.MeshHotspotSchema=z,exports.NoteItemSchema=ae,exports.NumericInputItemSchema=Ce,exports.OQSEFileSchema=De,exports.OQSEItemSchema=Z,exports.OQSEMetaSchema=N,exports.OpenEndedItemSchema=Se,exports.OptionalRichContentSchema=p,exports.PedagogySchema=M,exports.PersonObjectSchema=C,exports.PinOnImageItemSchema=ge,exports.PinOnModelItemSchema=X,exports.PlainTextSchema=u,exports.PolygonHotspotSchema=R,exports.PropertyKeySchema=E,exports.RawLatexSchema=d,exports.RectHotspotSchema=I,exports.RichContentSchema=f,exports.RubricCriterionSchema=q,exports.RubricSchema=J,exports.SPDXLicenseSchema=a,exports.SelectBlankObjectSchema=F,exports.ShortAnswerItemSchema=le,exports.SkillIdSchema=y,exports.SliderItemSchema=he,exports.SortItemsItemSchema=me,exports.SourceMaterialSchema=T,exports.SourceMaterialTypeSchema=w,exports.SourceReferenceSchema=ee,exports.SubtitleTrackSchema=b,exports.TagDefinitionDictionarySchema=ne,exports.TagDefinitionSchema=te,exports.TimelineDateSchema=s,exports.TimelineEventSchema=G,exports.TimelineItemSchema=ve,exports.TimelinePrecisionSchema=W,exports.TranslationObjectSchema=O,exports.TrueFalseItemSchema=oe,exports.UUIDSchema=r,exports.Vector3Schema=V,exports.choiceList=_,exports.getItemSchema=$,exports.safeValidateOQSEItem=Ae,exports.validateOQSEItem=ke;
|
package/dist/oqseValidation.js
CHANGED
|
@@ -10,7 +10,7 @@ var r = n.string().uuid({ message: "Invalid UUID format" }), i = n.string().min(
|
|
|
10
10
|
function _(e, t) {
|
|
11
11
|
return n.array(g).min(e, `Must have at least ${e} ${t}`).max(100, `Maximum 100 ${t}`).refine((e) => new Set(e.map((e) => e.id)).size === e.length, { message: "Choice IDs must be unique within the list" });
|
|
12
12
|
}
|
|
13
|
-
var v = (e) => new Set(e.map((e) => e.id)),
|
|
13
|
+
var v = (e) => new Set(e.map((e) => e.id)), y = n.string().max(128, "Skill ID must not be longer than 128 characters").regex(/^[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*$/, "Skill ID must be lowercase segments joined by dots (e.g. \"chess.tactics.fork\")"), b = n.looseObject({
|
|
14
14
|
lang: i,
|
|
15
15
|
value: n.string().min(1, "Subtitle URI must not be empty").regex(/\.(vtt|srt)(\?.*)?$/i, "Subtitle URI must end with .vtt or .srt"),
|
|
16
16
|
label: n.string().optional(),
|
|
@@ -19,7 +19,7 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
19
19
|
"subtitles",
|
|
20
20
|
"descriptions"
|
|
21
21
|
]).optional()
|
|
22
|
-
}),
|
|
22
|
+
}), x = n.looseObject({
|
|
23
23
|
type: n.enum([
|
|
24
24
|
"image",
|
|
25
25
|
"audio",
|
|
@@ -36,19 +36,19 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
36
36
|
start: n.number().nonnegative().optional(),
|
|
37
37
|
end: n.number().positive().optional(),
|
|
38
38
|
loop: n.boolean().optional(),
|
|
39
|
-
subtitles: n.array(
|
|
39
|
+
subtitles: n.array(b).optional(),
|
|
40
40
|
license: a.optional(),
|
|
41
41
|
attribution: n.string().optional(),
|
|
42
42
|
checksums: n.record(n.string(), n.string()).optional()
|
|
43
43
|
}).refine((e) => e.start !== void 0 && e.end !== void 0 ? e.start < e.end : !0, {
|
|
44
44
|
message: "Start time must be less than end time",
|
|
45
45
|
path: ["end"]
|
|
46
|
-
}),
|
|
46
|
+
}), S = n.record(l, x), C = n.looseObject({
|
|
47
47
|
name: u,
|
|
48
48
|
role: n.string().optional(),
|
|
49
49
|
email: n.string().email("Invalid email format").optional(),
|
|
50
50
|
url: c.optional()
|
|
51
|
-
}),
|
|
51
|
+
}), w = n.enum([
|
|
52
52
|
"url",
|
|
53
53
|
"doi",
|
|
54
54
|
"isbn",
|
|
@@ -59,9 +59,9 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
59
59
|
"image",
|
|
60
60
|
"model",
|
|
61
61
|
"other"
|
|
62
|
-
]),
|
|
62
|
+
]), T = n.looseObject({
|
|
63
63
|
id: u,
|
|
64
|
-
type:
|
|
64
|
+
type: w,
|
|
65
65
|
value: u,
|
|
66
66
|
title: u,
|
|
67
67
|
description: n.string().optional(),
|
|
@@ -85,7 +85,7 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
85
85
|
}, {
|
|
86
86
|
message: "For url, pdf, video, audio and image types, value must be a valid URL",
|
|
87
87
|
path: ["value"]
|
|
88
|
-
}),
|
|
88
|
+
}), ee = n.looseObject({
|
|
89
89
|
id: u,
|
|
90
90
|
location: n.string().optional(),
|
|
91
91
|
quote: f.optional()
|
|
@@ -132,21 +132,21 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
132
132
|
title: u.max(500, "Title must not be longer than 500 characters"),
|
|
133
133
|
description: f.max(5e3, "Description must not be longer than 5000 characters").optional(),
|
|
134
134
|
thumbnail: l.optional(),
|
|
135
|
-
assets:
|
|
135
|
+
assets: S.optional(),
|
|
136
136
|
ageMin: n.number().int().nonnegative().optional(),
|
|
137
137
|
ageMax: n.number().int().nonnegative().optional(),
|
|
138
138
|
subject: n.string().optional(),
|
|
139
139
|
createdAt: o,
|
|
140
140
|
updatedAt: o,
|
|
141
|
-
author:
|
|
142
|
-
contributors: n.array(
|
|
141
|
+
author: C.optional(),
|
|
142
|
+
contributors: n.array(C).optional(),
|
|
143
143
|
license: a.optional(),
|
|
144
144
|
licenseUrl: c.optional(),
|
|
145
145
|
requirements: D.optional(),
|
|
146
146
|
tags: n.array(u).optional(),
|
|
147
147
|
tagDefinitions: ne.optional(),
|
|
148
148
|
translations: n.array(O).optional(),
|
|
149
|
-
sourceMaterials: n.array(
|
|
149
|
+
sourceMaterials: n.array(T).optional(),
|
|
150
150
|
estimatedTime: n.number().positive().optional(),
|
|
151
151
|
prerequisites: n.array(k).optional(),
|
|
152
152
|
relatedSets: n.array(k).optional(),
|
|
@@ -161,17 +161,17 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
161
161
|
}), P = n.looseObject({
|
|
162
162
|
id: r,
|
|
163
163
|
type: n.string(),
|
|
164
|
-
assets:
|
|
164
|
+
assets: S.optional(),
|
|
165
165
|
lang: i.optional(),
|
|
166
166
|
tags: n.array(u).optional(),
|
|
167
|
-
skills: n.array(
|
|
167
|
+
skills: n.array(y).max(20, "Maximum 20 skills per item").optional(),
|
|
168
168
|
topic: u.optional(),
|
|
169
169
|
difficulty: n.number().int().min(1, "Difficulty must be at least 1").max(5, "Difficulty must be at most 5").optional(),
|
|
170
170
|
timeLimit: n.number().positive("Time limit must be a positive number").optional(),
|
|
171
171
|
hints: n.array(f.max(2e3, "Hint must not be longer than 2000 characters")).max(20, "Maximum 20 hints per item").optional(),
|
|
172
172
|
explanation: f.max(1e4, "Explanation must not be longer than 10000 characters").optional(),
|
|
173
173
|
incorrectFeedback: f.optional(),
|
|
174
|
-
sources: n.array(
|
|
174
|
+
sources: n.array(ee).optional(),
|
|
175
175
|
relatedItems: n.array(r).optional(),
|
|
176
176
|
dependencyItems: n.array(r).optional(),
|
|
177
177
|
pedagogy: M.optional(),
|
|
@@ -261,12 +261,12 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
261
261
|
}, {
|
|
262
262
|
message: "Sum of percentages of all criteria must be greater than 0",
|
|
263
263
|
path: ["criteria"]
|
|
264
|
-
}),
|
|
264
|
+
}), ae = P.extend({
|
|
265
265
|
type: n.literal("note"),
|
|
266
266
|
title: n.string().optional(),
|
|
267
267
|
content: f.max(1e5, "Content must not be longer than 100000 characters"),
|
|
268
268
|
hiddenContent: f.max(1e5, "Hidden content must not be longer than 100000 characters").optional()
|
|
269
|
-
}),
|
|
269
|
+
}), Y = P.extend({
|
|
270
270
|
type: n.literal("flashcard"),
|
|
271
271
|
front: f.max(1e4, "Front side must not be longer than 10000 characters"),
|
|
272
272
|
back: f.max(1e4, "Back side must not be longer than 10000 characters")
|
|
@@ -376,11 +376,11 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
376
376
|
}), pe = P.extend({
|
|
377
377
|
type: n.literal("match-complex"),
|
|
378
378
|
question: p,
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
prompts: _(1, "prompts"),
|
|
380
|
+
matches: _(1, "matches"),
|
|
381
381
|
connections: n.array(n.tuple([h, h])).min(1, "Must have at least 1 connection"),
|
|
382
382
|
minCorrect: n.number().int().positive().optional()
|
|
383
|
-
}).refine((e) => e.connections.every(([t, n]) => v(e.
|
|
383
|
+
}).refine((e) => e.connections.every(([t, n]) => v(e.prompts).has(t) && v(e.matches).has(n)), {
|
|
384
384
|
message: "Some connection references a non-existent item",
|
|
385
385
|
path: ["connections"]
|
|
386
386
|
}).refine((e) => new Set(e.connections.map(([e, t]) => `${e}\u0000${t}`)).size === e.connections.length, {
|
|
@@ -466,9 +466,7 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
466
466
|
}), be = P.extend({
|
|
467
467
|
type: n.literal("math-input"),
|
|
468
468
|
question: f.max(1e4, "Question must not be longer than 10000 characters"),
|
|
469
|
-
|
|
470
|
-
alternativeAnswers: n.array(d).optional(),
|
|
471
|
-
tolerance: n.number().nonnegative().optional()
|
|
469
|
+
correctAnswers: n.array(d).min(1, "Must have at least 1 correct answer")
|
|
472
470
|
}), xe = P.extend({
|
|
473
471
|
type: n.literal("diagram-label"),
|
|
474
472
|
question: f.max(1e4, "Question must not be longer than 10000 characters"),
|
|
@@ -519,8 +517,8 @@ var v = (e) => new Set(e.map((e) => e.id)), ee = n.string().max(128, "Skill ID m
|
|
|
519
517
|
correctAnswers: n.array(n.array(u).min(1, "Move sequence must not be empty")).min(1, "Must have at least 1 correct move sequence"),
|
|
520
518
|
elo: n.number().int().nonnegative("ELO must be non-negative integer").optional()
|
|
521
519
|
}), Z = n.discriminatedUnion("type", [
|
|
522
|
-
Y,
|
|
523
520
|
ae,
|
|
521
|
+
Y,
|
|
524
522
|
oe,
|
|
525
523
|
se,
|
|
526
524
|
ce,
|
|
@@ -583,4 +581,4 @@ function Ae(t) {
|
|
|
583
581
|
};
|
|
584
582
|
}
|
|
585
583
|
//#endregion
|
|
586
|
-
export { c as AbsoluteURLSchema, Ee as AnyOQSEItemSchema,
|
|
584
|
+
export { c as AbsoluteURLSchema, Ee as AnyOQSEItemSchema, S as AssetDictionarySchema, l as AssetKeySchema, P as BaseItemSchema, m as BlankTokenSchema, A as BloomLevelSchema, H as CameraSetupSchema, U as CategorizeEntrySchema, _e as CategorizeItemSchema, we as ChessPuzzleItemSchema, h as ChoiceIdSchema, g as ChoiceSchema, L as CircleHotspotSchema, j as CognitiveLoadSchema, Q as CustomItemSchema, Te as CustomItemTypeSchema, xe as DiagramLabelItemSchema, K as DiagramZoneSchema, re as FeatureKeySchema, D as FeatureProfileSchema, ue as FillInBlanksItemSchema, de as FillInSelectItemSchema, Y as FlashcardItemSchema, B as Hotspot2DSchema, ie as HotspotObjectSchema, o as ISO8601DateTimeSchema, i as LanguageCodeSchema, k as LinkedSetObjectSchema, ce as MCQMultiItemSchema, se as MCQSingleItemSchema, pe as MatchComplexItemSchema, fe as MatchPairsItemSchema, be as MathInputItemSchema, ye as MatrixItemSchema, x as MediaObjectSchema, z as MeshHotspotSchema, ae as NoteItemSchema, Ce as NumericInputItemSchema, De as OQSEFileSchema, Z as OQSEItemSchema, N as OQSEMetaSchema, Se as OpenEndedItemSchema, p as OptionalRichContentSchema, M as PedagogySchema, C as PersonObjectSchema, ge as PinOnImageItemSchema, X as PinOnModelItemSchema, u as PlainTextSchema, R as PolygonHotspotSchema, E as PropertyKeySchema, d as RawLatexSchema, I as RectHotspotSchema, f as RichContentSchema, q as RubricCriterionSchema, J as RubricSchema, a as SPDXLicenseSchema, F as SelectBlankObjectSchema, le as ShortAnswerItemSchema, y as SkillIdSchema, he as SliderItemSchema, me as SortItemsItemSchema, T as SourceMaterialSchema, w as SourceMaterialTypeSchema, ee as SourceReferenceSchema, b as SubtitleTrackSchema, ne as TagDefinitionDictionarySchema, te as TagDefinitionSchema, s as TimelineDateSchema, G as TimelineEventSchema, ve as TimelineItemSchema, W as TimelinePrecisionSchema, O as TranslationObjectSchema, oe as TrueFalseItemSchema, r as UUIDSchema, V as Vector3Schema, _ as choiceList, $ as getItemSchema, Ae as safeValidateOQSEItem, ke as validateOQSEItem };
|
package/dist/richTextFields.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(e,t,n){typeof n==`string`&&e.push([t,n])}function n(e,n,r){Array.isArray(r)&&r.forEach((r,i)=>t(e,[...n,i],r))}function r(n,r,i){Array.isArray(i)&&i.forEach((i,a)=>{e(i)&&(t(n,[...r,a,`text`],i.text),t(n,[...r,a,`explanation`],i.explanation))})}function i(e,n,r){for(let[i,a]of Object.entries(r??{}))t(e,[...n,i,`transcript`],a.transcript),t(e,[...n,i,`caption`],a.caption)}function a(e){let n=[];return t(n,[`meta`,`description`],e.description),i(n,[`meta`,`assets`],e.assets),n}function o(e){let a=[];switch(n(a,[`hints`],e.hints),t(a,[`explanation`],e.explanation),t(a,[`incorrectFeedback`],e.incorrectFeedback),e.sources?.forEach((e,n)=>t(a,[`sources`,n,`quote`],e.quote)),i(a,[`assets`],e.assets),`question`in e&&t(a,[`question`],e.question),e.type){case`note`:t(a,[`content`],e.content),t(a,[`hiddenContent`],e.hiddenContent);break;case`flashcard`:t(a,[`front`],e.front),t(a,[`back`],e.back);break;case`mcq-single`:case`mcq-multi`:r(a,[`options`],e.options);break;case`fill-in-blanks`:t(a,[`text`],e.text);break;case`fill-in-select`:t(a,[`text`],e.text);for(let[t,n]of Object.entries(e.blanks))r(a,[`blanks`,t,`options`],n.options);break;case`match-pairs`:r(a,[`prompts`],e.prompts),r(a,[`matches`],e.matches);break;case`match-complex`:r(a,[`
|
|
1
|
+
function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(e,t,n){typeof n==`string`&&e.push([t,n])}function n(e,n,r){Array.isArray(r)&&r.forEach((r,i)=>t(e,[...n,i],r))}function r(n,r,i){Array.isArray(i)&&i.forEach((i,a)=>{e(i)&&(t(n,[...r,a,`text`],i.text),t(n,[...r,a,`explanation`],i.explanation))})}function i(e,n,r){for(let[i,a]of Object.entries(r??{}))t(e,[...n,i,`transcript`],a.transcript),t(e,[...n,i,`caption`],a.caption)}function a(e){let n=[];return t(n,[`meta`,`description`],e.description),i(n,[`meta`,`assets`],e.assets),n}function o(e){let a=[];switch(n(a,[`hints`],e.hints),t(a,[`explanation`],e.explanation),t(a,[`incorrectFeedback`],e.incorrectFeedback),e.sources?.forEach((e,n)=>t(a,[`sources`,n,`quote`],e.quote)),i(a,[`assets`],e.assets),`question`in e&&t(a,[`question`],e.question),e.type){case`note`:t(a,[`content`],e.content),t(a,[`hiddenContent`],e.hiddenContent);break;case`flashcard`:t(a,[`front`],e.front),t(a,[`back`],e.back);break;case`mcq-single`:case`mcq-multi`:r(a,[`options`],e.options);break;case`fill-in-blanks`:t(a,[`text`],e.text);break;case`fill-in-select`:t(a,[`text`],e.text);for(let[t,n]of Object.entries(e.blanks))r(a,[`blanks`,t,`options`],n.options);break;case`match-pairs`:r(a,[`prompts`],e.prompts),r(a,[`matches`],e.matches);break;case`match-complex`:r(a,[`prompts`],e.prompts),r(a,[`matches`],e.matches);break;case`sort-items`:r(a,[`items`],e.items);break;case`categorize`:r(a,[`categories`],e.categories),e.items.forEach((e,n)=>t(a,[`items`,n,`text`],e.text));break;case`timeline`:e.events.forEach((e,n)=>t(a,[`events`,n,`text`],e.text));break;case`diagram-label`:r(a,[`labels`],e.labels);break;case`matrix`:r(a,[`rows`],e.rows),r(a,[`columns`],e.columns);break;case`open-ended`:t(a,[`sampleAnswer`],e.sampleAnswer)}return a}exports.itemRichText=o,exports.metaRichText=a;
|
package/dist/richTextFields.js
CHANGED
|
@@ -68,7 +68,7 @@ function o(e) {
|
|
|
68
68
|
r(a, ["prompts"], e.prompts), r(a, ["matches"], e.matches);
|
|
69
69
|
break;
|
|
70
70
|
case "match-complex":
|
|
71
|
-
r(a, ["
|
|
71
|
+
r(a, ["prompts"], e.prompts), r(a, ["matches"], e.matches);
|
|
72
72
|
break;
|
|
73
73
|
case "sort-items":
|
|
74
74
|
r(a, ["items"], e.items);
|
package/dist/src/assets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MediaObject, OQSEAnyItem, OQSEMeta } from './oqse';
|
|
1
|
+
import { MediaObject, OQSEAnyItem, OQSEMeta } from './oqse.js';
|
|
2
2
|
/**
|
|
3
3
|
* Looks up a media object for `<asset:key />` or `targetAsset`: first in the
|
|
4
4
|
* item's own `assets`, then in `meta.assets`. The key is case-insensitive.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { OQSEAnyItem } from './oqse';
|
|
1
|
+
import { OQSEAnyItem } from './oqse.js';
|
|
2
2
|
export declare function checkAnswer(item: OQSEAnyItem, answer: unknown): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MediaObject, OQSEFile } from './oqse';
|
|
2
|
-
import { OQSEManifest } from './manifest';
|
|
1
|
+
import { MediaObject, OQSEFile } from './oqse.js';
|
|
2
|
+
import { OQSEManifest } from './manifest.js';
|
|
3
3
|
export interface UnsupportedAsset {
|
|
4
4
|
/** `meta.assets` key, or `items[i].assets` key prefixed with the item path. */
|
|
5
5
|
path: string;
|
package/dist/src/features.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OQSEFile } from './oqse';
|
|
1
|
+
import { OQSEFile } from './oqse.js';
|
|
2
2
|
export type OQSEIssueSeverity = 'error' | 'warning';
|
|
3
3
|
export type OQSEIssueCode = 'INVALID_ROOT' | 'INVALID_VERSION' | 'UNSUPPORTED_VERSION' | 'INVALID_META' | 'TOO_MANY_ITEMS' | 'INVALID_ITEM' | 'UNKNOWN_ITEM_TYPE' | 'DUPLICATE_ID' | 'DANGLING_REFERENCE' | 'MISSING_ASSET' | 'MISSING_TARGET_ASSET' | 'INVALID_TARGET_ASSET' | 'MISSING_THUMBNAIL_ASSET' | 'ASSET_KEY_NORMALIZED' | 'ASSET_KEY_COLLISION' | 'ASSET_SHADOWED' | 'RAW_HTML_NOT_ALLOWED' | 'NOTE_HEADING_LEVEL' | 'CHESS_SEQUENCE_LENGTH' | 'UNKNOWN_FEATURE' | 'MISSING_ALT_TEXT' | 'INVALID_IMAGE_URL' | 'BLANK_OUTSIDE_TEXT' | 'UNDECLARED_FEATURE' | 'LOOKS_LIKE_LATEX' | 'SECURITY_LIMIT';
|
|
4
4
|
/** One entry of the structured error log (see "Structured Error Log" in the specification). */
|
package/dist/src/header.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OQSEMeta } from './oqse';
|
|
1
|
+
import { OQSEMeta } from './oqse.js';
|
|
2
2
|
type OQSEHeaderMetaProjection = Pick<OQSEMeta, 'id' | 'title' | 'language' | 'updatedAt' | 'description' | 'author' | 'subject' | 'tags' | 'createdAt' | 'estimatedTime' | 'requirements'>;
|
|
3
3
|
export type OQSEHeader = Omit<OQSEHeaderMetaProjection, 'createdAt'> & Partial<Pick<OQSEHeaderMetaProjection, 'createdAt'>> & {
|
|
4
4
|
url: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { OQSEHeader } from './header';
|
|
2
|
+
import { OQSEHeader } from './header.js';
|
|
3
3
|
export declare const OQSEHeaderSchema: z.ZodType<OQSEHeader>;
|
|
4
4
|
export declare function validateOQSEHeader(data: unknown): OQSEHeader;
|
|
5
5
|
export declare function safeValidateOQSEHeader(data: unknown): ReturnType<typeof OQSEHeaderSchema.safeParse>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
export * from './oqse';
|
|
2
|
-
export * from './oqseValidation';
|
|
3
|
-
export * from './manifest';
|
|
4
|
-
export * from './manifestValidation';
|
|
5
|
-
export * from './progress';
|
|
6
|
-
export * from './progressValidation';
|
|
7
|
-
export * from './header';
|
|
8
|
-
export * from './headerValidation';
|
|
9
|
-
export * from './utils';
|
|
10
|
-
export * from './richTextProcessor';
|
|
11
|
-
export * from './markdownSet';
|
|
12
|
-
export { shiftHeadings } from './markdownUtils';
|
|
13
|
-
export * from './fileValidation';
|
|
14
|
-
export * from './richTextFields';
|
|
15
|
-
export * from './assets';
|
|
16
|
-
export * from './answers';
|
|
17
|
-
export * from './checkAnswer';
|
|
18
|
-
export * from './plainText';
|
|
19
|
-
export * from './features';
|
|
20
|
-
export * from './compatibility';
|
|
1
|
+
export * from './oqse.js';
|
|
2
|
+
export * from './oqseValidation.js';
|
|
3
|
+
export * from './manifest.js';
|
|
4
|
+
export * from './manifestValidation.js';
|
|
5
|
+
export * from './progress.js';
|
|
6
|
+
export * from './progressValidation.js';
|
|
7
|
+
export * from './header.js';
|
|
8
|
+
export * from './headerValidation.js';
|
|
9
|
+
export * from './utils.js';
|
|
10
|
+
export * from './richTextProcessor.js';
|
|
11
|
+
export * from './markdownSet.js';
|
|
12
|
+
export { shiftHeadings } from './markdownUtils.js';
|
|
13
|
+
export * from './fileValidation.js';
|
|
14
|
+
export * from './richTextFields.js';
|
|
15
|
+
export * from './assets.js';
|
|
16
|
+
export * from './answers.js';
|
|
17
|
+
export * from './checkAnswer.js';
|
|
18
|
+
export * from './plainText.js';
|
|
19
|
+
export * from './features.js';
|
|
20
|
+
export * from './compatibility.js';
|
package/dist/src/manifest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FeatureProfile, PersonObject } from './oqse';
|
|
2
|
-
export type { FeatureProfile } from './oqse';
|
|
1
|
+
import { FeatureProfile, PersonObject } from './oqse.js';
|
|
2
|
+
export type { FeatureProfile } from './oqse.js';
|
|
3
3
|
/**
|
|
4
4
|
* Official actions from the OQSE Action Registry (Section 2.1.3).
|
|
5
5
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OQSEFile } from './oqse';
|
|
2
|
-
import { OQSEIssue } from './fileValidation';
|
|
1
|
+
import { OQSEFile } from './oqse.js';
|
|
2
|
+
import { OQSEIssue } from './fileValidation.js';
|
|
3
3
|
/** Callout marker that introduces `hiddenContent` (case-insensitive when parsing). */
|
|
4
4
|
export declare const HIDDEN_CALLOUT_MARKER = "[!hidden]";
|
|
5
5
|
/** Default heading level of notes: `#` set title, `##` chapters, `###` notes. */
|
package/dist/src/oqse.d.ts
CHANGED
|
@@ -692,11 +692,11 @@ export interface MatchComplexItem extends BaseItem {
|
|
|
692
692
|
type: 'match-complex';
|
|
693
693
|
/** Instructions (Rich Content) */
|
|
694
694
|
question?: string;
|
|
695
|
-
/**
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
|
|
699
|
-
/** Correct connections as `[
|
|
695
|
+
/** What the learner connects from (like the prompts of `match-pairs`). Min 1. */
|
|
696
|
+
prompts: Choice[];
|
|
697
|
+
/** What the prompts are connected to (may include distractors). Min 1. */
|
|
698
|
+
matches: Choice[];
|
|
699
|
+
/** Correct connections as `[promptId, matchId]` (any number per prompt and per match). Min 1. */
|
|
700
700
|
connections: Array<[string, string]>;
|
|
701
701
|
/** Minimum number of correct connections required for success */
|
|
702
702
|
minCorrect?: number;
|
|
@@ -799,12 +799,8 @@ export interface MathInputItem extends BaseItem {
|
|
|
799
799
|
type: 'math-input';
|
|
800
800
|
/** Question text (Rich Content) */
|
|
801
801
|
question: string;
|
|
802
|
-
/** Correct
|
|
803
|
-
|
|
804
|
-
/** Other raw LaTeX answers (without `$` delimiters) that should also be considered correct */
|
|
805
|
-
alternativeAnswers?: string[];
|
|
806
|
-
/** For purely numeric answers, allowed numeric deviation */
|
|
807
|
-
tolerance?: number;
|
|
802
|
+
/** Correct answers as raw LaTeX without `$` delimiters (e.g., ["2x+2", "2+2x"]). Min 1. */
|
|
803
|
+
correctAnswers: string[];
|
|
808
804
|
}
|
|
809
805
|
/**
|
|
810
806
|
* Diagram Labeling
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { OQSEAnyItem } from './oqse';
|
|
2
|
+
import { OQSEAnyItem } from './oqse.js';
|
|
3
3
|
/**
|
|
4
4
|
* UUID validation: any valid UUID (new IDs should be UUIDv7, not required)
|
|
5
5
|
*/
|
|
@@ -1471,12 +1471,12 @@ export declare const MatchComplexItemSchema: z.ZodObject<{
|
|
|
1471
1471
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1472
1472
|
type: z.ZodLiteral<"match-complex">;
|
|
1473
1473
|
question: z.ZodOptional<z.ZodString>;
|
|
1474
|
-
|
|
1474
|
+
prompts: z.ZodArray<z.ZodObject<{
|
|
1475
1475
|
id: z.ZodString;
|
|
1476
1476
|
text: z.ZodString;
|
|
1477
1477
|
explanation: z.ZodOptional<z.ZodString>;
|
|
1478
1478
|
}, z.core.$loose>>;
|
|
1479
|
-
|
|
1479
|
+
matches: z.ZodArray<z.ZodObject<{
|
|
1480
1480
|
id: z.ZodString;
|
|
1481
1481
|
text: z.ZodString;
|
|
1482
1482
|
explanation: z.ZodOptional<z.ZodString>;
|
|
@@ -2099,9 +2099,7 @@ export declare const MathInputItemSchema: z.ZodObject<{
|
|
|
2099
2099
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
2100
2100
|
type: z.ZodLiteral<"math-input">;
|
|
2101
2101
|
question: z.ZodString;
|
|
2102
|
-
|
|
2103
|
-
alternativeAnswers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2104
|
-
tolerance: z.ZodOptional<z.ZodNumber>;
|
|
2102
|
+
correctAnswers: z.ZodArray<z.ZodString>;
|
|
2105
2103
|
}, z.core.$loose>;
|
|
2106
2104
|
/**
|
|
2107
2105
|
* Diagram Label Item
|
|
@@ -3365,12 +3363,12 @@ export declare const OQSEItemSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
3365
3363
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3366
3364
|
type: z.ZodLiteral<"match-complex">;
|
|
3367
3365
|
question: z.ZodOptional<z.ZodString>;
|
|
3368
|
-
|
|
3366
|
+
prompts: z.ZodArray<z.ZodObject<{
|
|
3369
3367
|
id: z.ZodString;
|
|
3370
3368
|
text: z.ZodString;
|
|
3371
3369
|
explanation: z.ZodOptional<z.ZodString>;
|
|
3372
3370
|
}, z.core.$loose>>;
|
|
3373
|
-
|
|
3371
|
+
matches: z.ZodArray<z.ZodObject<{
|
|
3374
3372
|
id: z.ZodString;
|
|
3375
3373
|
text: z.ZodString;
|
|
3376
3374
|
explanation: z.ZodOptional<z.ZodString>;
|
|
@@ -3965,9 +3963,7 @@ export declare const OQSEItemSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
3965
3963
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3966
3964
|
type: z.ZodLiteral<"math-input">;
|
|
3967
3965
|
question: z.ZodString;
|
|
3968
|
-
|
|
3969
|
-
alternativeAnswers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3970
|
-
tolerance: z.ZodOptional<z.ZodNumber>;
|
|
3966
|
+
correctAnswers: z.ZodArray<z.ZodString>;
|
|
3971
3967
|
}, z.core.$loose>, z.ZodObject<{
|
|
3972
3968
|
id: z.ZodString;
|
|
3973
3969
|
assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -5283,12 +5279,12 @@ export declare const AnyOQSEItemSchema: z.ZodUnion<readonly [z.ZodDiscriminatedU
|
|
|
5283
5279
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
5284
5280
|
type: z.ZodLiteral<"match-complex">;
|
|
5285
5281
|
question: z.ZodOptional<z.ZodString>;
|
|
5286
|
-
|
|
5282
|
+
prompts: z.ZodArray<z.ZodObject<{
|
|
5287
5283
|
id: z.ZodString;
|
|
5288
5284
|
text: z.ZodString;
|
|
5289
5285
|
explanation: z.ZodOptional<z.ZodString>;
|
|
5290
5286
|
}, z.core.$loose>>;
|
|
5291
|
-
|
|
5287
|
+
matches: z.ZodArray<z.ZodObject<{
|
|
5292
5288
|
id: z.ZodString;
|
|
5293
5289
|
text: z.ZodString;
|
|
5294
5290
|
explanation: z.ZodOptional<z.ZodString>;
|
|
@@ -5883,9 +5879,7 @@ export declare const AnyOQSEItemSchema: z.ZodUnion<readonly [z.ZodDiscriminatedU
|
|
|
5883
5879
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
5884
5880
|
type: z.ZodLiteral<"math-input">;
|
|
5885
5881
|
question: z.ZodString;
|
|
5886
|
-
|
|
5887
|
-
alternativeAnswers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5888
|
-
tolerance: z.ZodOptional<z.ZodNumber>;
|
|
5882
|
+
correctAnswers: z.ZodArray<z.ZodString>;
|
|
5889
5883
|
}, z.core.$loose>, z.ZodObject<{
|
|
5890
5884
|
id: z.ZodString;
|
|
5891
5885
|
assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -7314,12 +7308,12 @@ export declare const OQSEFileSchema: z.ZodObject<{
|
|
|
7314
7308
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
7315
7309
|
type: z.ZodLiteral<"match-complex">;
|
|
7316
7310
|
question: z.ZodOptional<z.ZodString>;
|
|
7317
|
-
|
|
7311
|
+
prompts: z.ZodArray<z.ZodObject<{
|
|
7318
7312
|
id: z.ZodString;
|
|
7319
7313
|
text: z.ZodString;
|
|
7320
7314
|
explanation: z.ZodOptional<z.ZodString>;
|
|
7321
7315
|
}, z.core.$loose>>;
|
|
7322
|
-
|
|
7316
|
+
matches: z.ZodArray<z.ZodObject<{
|
|
7323
7317
|
id: z.ZodString;
|
|
7324
7318
|
text: z.ZodString;
|
|
7325
7319
|
explanation: z.ZodOptional<z.ZodString>;
|
|
@@ -7914,9 +7908,7 @@ export declare const OQSEFileSchema: z.ZodObject<{
|
|
|
7914
7908
|
appSpecific: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
7915
7909
|
type: z.ZodLiteral<"math-input">;
|
|
7916
7910
|
question: z.ZodString;
|
|
7917
|
-
|
|
7918
|
-
alternativeAnswers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7919
|
-
tolerance: z.ZodOptional<z.ZodNumber>;
|
|
7911
|
+
correctAnswers: z.ZodArray<z.ZodString>;
|
|
7920
7912
|
}, z.core.$loose>, z.ZodObject<{
|
|
7921
7913
|
id: z.ZodString;
|
|
7922
7914
|
assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
package/dist/src/plainText.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { LastAnswerObject, OQSEProgress, ProgressMeta, ProgressRecord, StatsObject } from './progress';
|
|
2
|
+
import { LastAnswerObject, OQSEProgress, ProgressMeta, ProgressRecord, StatsObject } from './progress.js';
|
|
3
3
|
export declare const StatsObjectSchema: z.ZodType<StatsObject>;
|
|
4
4
|
export declare const LastAnswerObjectSchema: z.ZodType<LastAnswerObject>;
|
|
5
5
|
export declare const ProgressRecordSchema: z.ZodType<ProgressRecord>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OQSEAnyItem, OQSEMeta } from './oqse';
|
|
1
|
+
import { OQSEAnyItem, OQSEMeta } from './oqse.js';
|
|
2
2
|
type Path = (string | number)[];
|
|
3
3
|
/** A Rich Content field: its path (relative to the item, or from the root for meta) and its text. */
|
|
4
4
|
export type TextEntry = [Path, string];
|
package/oqse-header.md
CHANGED
|
@@ -54,3 +54,9 @@ An OQSEHeader represents one catalog entry that points to a full OQSE set.
|
|
|
54
54
|
|
|
55
55
|
- OQSEH is intentionally lightweight and should remain a projection of OQSE `meta` plus `url`.
|
|
56
56
|
- Header consumers should use `url` as the canonical pointer to the full set payload.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Changelog
|
|
61
|
+
|
|
62
|
+
OQSEH 1.0 is part of OQSE 1.0 (2026-10-14); its changes are listed in the [OQSE changelog](./oqse.md#version-10-2026-10-14).
|
package/oqse-manifest.md
CHANGED
|
@@ -247,3 +247,9 @@ For the full semantics of the capabilities object, see [Capabilities Object](#ca
|
|
|
247
247
|
**`capabilities.itemProperties` / `capabilities.metaProperties`:**
|
|
248
248
|
- Each value SHOULD appear in the relevant registry or carry the `x-` prefix.
|
|
249
249
|
- Unrecognized values without an `x-` prefix SHOULD trigger a warning but MUST NOT cause manifest rejection.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Changelog
|
|
254
|
+
|
|
255
|
+
OQSEM 1.0 is part of OQSE 1.0 (2026-10-14); its changes are listed in the [OQSE changelog](./oqse.md#version-10-2026-10-14).
|
package/oqse-progress.md
CHANGED
|
@@ -169,4 +169,10 @@ Note that an item belonging to the set might be entirely absent from `records`.
|
|
|
169
169
|
|
|
170
170
|
**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.
|
|
171
171
|
|
|
172
|
-
**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.
|
|
172
|
+
**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.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Changelog
|
|
177
|
+
|
|
178
|
+
OQSEP 1.0 is part of OQSE 1.0 (2026-10-14); its changes are listed in the [OQSE changelog](./oqse.md#version-10-2026-10-14).
|
package/oqse.md
CHANGED
|
@@ -33,6 +33,7 @@ In this specification, the keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD
|
|
|
33
33
|
* [Item Types](#item-types-itemtype)
|
|
34
34
|
* [Helper Data Structures](#helper-data-structures)
|
|
35
35
|
* [Complete Example File](#complete-example-file)
|
|
36
|
+
* [Canonical and Authoring Formats](#canonical-and-authoring-formats)
|
|
36
37
|
* [OQSE Markdown (Note Sets)](#oqse-markdown-note-sets)
|
|
37
38
|
* [Validation Rules and Constraints](#validation-rules-and-constraints)
|
|
38
39
|
* [Error Handling](#error-handling-error-handling-policy)
|
|
@@ -481,7 +482,7 @@ If same key exists in both scopes, local definition (`item.assets`) takes preced
|
|
|
481
482
|
|
|
482
483
|
## Choices and Answers by ID
|
|
483
484
|
|
|
484
|
-
Lists that the learner selects from or arranges are lists of **choices**: options of `mcq-single` / `mcq-multi` and of `fill-in-select` blanks, `items` of `sort-items`, `prompts` and `matches` of `match-pairs
|
|
485
|
+
Lists that the learner selects from or arranges are lists of **choices**: options of `mcq-single` / `mcq-multi` and of `fill-in-select` blanks, `items` of `sort-items`, `prompts` and `matches` of `match-pairs` and `match-complex`, `categories` of `categorize`, `rows` and `columns` of `matrix` and `labels` of `diagram-label`.
|
|
485
486
|
|
|
486
487
|
```json
|
|
487
488
|
{ "id": "b", "text": "Praha", "explanation": "The capital since the 10th century." }
|
|
@@ -535,7 +536,7 @@ This section defines what a learner's answer to each item type looks like and wh
|
|
|
535
536
|
| `fill-in-blanks` | Object `{ token: string }` | For every token in `blanks`, the string matches one of its accepted answers ([Comparing Typed Answers](#comparing-typed-answers)). |
|
|
536
537
|
| `fill-in-select` | Object `{ token: choiceId }` | For every token in `blanks`, the choice is its `correctId`. |
|
|
537
538
|
| `match-pairs` | Object `{ promptId: matchId }` | Every prompt is mapped to its match in `pairs`. |
|
|
538
|
-
| `match-complex` | Array of `[
|
|
539
|
+
| `match-complex` | Array of `[promptId, matchId]` pairs (oriented: prompt first) | It contains no pair outside `connections`, and at least `minCorrect` (default: all) different pairs of `connections`. |
|
|
539
540
|
| `sort-items` | Array of item IDs in the learner's order | It equals `correctOrder`. |
|
|
540
541
|
| `slider` | Number | `|answer − correctAnswer| <= tolerance`. |
|
|
541
542
|
| `numeric-input` | Number | `|answer − correctAnswer| <= tolerance` (see [`numeric-input`](#type-numeric-input-numeric-input)). |
|
|
@@ -544,7 +545,7 @@ This section defines what a learner's answer to each item type looks like and wh
|
|
|
544
545
|
| `categorize` | Object `{ entryId: categoryId }` | Every entry is mapped to its `correctCategoryId`. |
|
|
545
546
|
| `timeline` | Array of event IDs in the learner's order | Every event appears exactly once and the order is correct ([see TimelineEvent](#timelineevent-for-timeline)). |
|
|
546
547
|
| `matrix` | Array of `[rowId, columnId]` cells | It contains exactly the cells of `correctCells` (no duplicates). |
|
|
547
|
-
| `math-input` | LaTeX string | It equals
|
|
548
|
+
| `math-input` | LaTeX string | It equals one of `correctAnswers` by the string comparison rules of [`math-input`](#type-math-input-math-input) (`$` delimiters typed by the learner are ignored), or is mathematically equivalent when the application checks symbolically. |
|
|
548
549
|
| `diagram-label` | Object `{ zoneId: labelId }`; with `requireTyping`, `{ zoneId: string }` | Every zone gets its `correctLabelId`; with `requireTyping`, every typed string matches the text of the zone's correct label ([Comparing Typed Answers](#comparing-typed-answers)). |
|
|
549
550
|
| `chess-puzzle` | Array of moves in SAN: the learner's moves and the opponent's replies, as in `correctAnswers` | It equals one of `correctAnswers` move by move, after normalizing each move ([see chess-puzzle](#type-chess-puzzle-chess-puzzle)). |
|
|
550
551
|
| `flashcard` | Self-rating: integer `1`–`4` on the [OQSEP confidence scale](./oqse-progress.md#confidence-scale) | The rating is `3` or `4` (recalled). Applications SHOULD offer all four ratings. |
|
|
@@ -781,9 +782,10 @@ A set consisting only of notes can also be written as one Markdown document, see
|
|
|
781
782
|
### `type: "match-complex"` (Advanced Matching)
|
|
782
783
|
|
|
783
784
|
* `question` (string, optional): **Rich Content.** Instructions.
|
|
784
|
-
* `
|
|
785
|
-
* `
|
|
786
|
-
* `connections` (string[][], required): Correct pairs as `[
|
|
785
|
+
* `prompts` (Choice[], required): What the learner connects from (as in `match-pairs`).
|
|
786
|
+
* `matches` (Choice[], required): What the prompts are connected to; may contain distractors.
|
|
787
|
+
* `connections` (string[][], required): Correct pairs as `[promptId, matchId]`.
|
|
788
|
+
* **Note:** The names say what the lists mean, not where they are shown: an application may lay them out side by side, one above the other, or in any other way.
|
|
787
789
|
* `minCorrect` (number, optional): Minimum number of correct connections required for success. If not specified, user must find all connections in `connections`.
|
|
788
790
|
* **Note:** This type explicitly supports more complex graph structures:
|
|
789
791
|
* Multiple connections are allowed in both directions (`1:N`, `N:1`, `N:M`).
|
|
@@ -797,12 +799,12 @@ A set consisting only of notes can also be written as one Markdown document, see
|
|
|
797
799
|
"id": "019aa5f5-907f-7373-aeed-128158901d17",
|
|
798
800
|
"type": "match-complex",
|
|
799
801
|
"question": "Match rockets to their main engines (some options are extra):",
|
|
800
|
-
"
|
|
802
|
+
"prompts": [
|
|
801
803
|
{ "id": "starship", "text": "Starship" },
|
|
802
804
|
{ "id": "falcon9", "text": "Falcon 9" },
|
|
803
805
|
{ "id": "sls", "text": "Space Launch System" }
|
|
804
806
|
],
|
|
805
|
-
"
|
|
807
|
+
"matches": [
|
|
806
808
|
{ "id": "raptor", "text": "Raptor" },
|
|
807
809
|
{ "id": "merlin", "text": "Merlin" },
|
|
808
810
|
{ "id": "rs25", "text": "RS-25" },
|
|
@@ -992,10 +994,8 @@ In the example above, `BE-4` has no connection: it is a distractor.
|
|
|
992
994
|
### `type: "math-input"` (Math Input)
|
|
993
995
|
|
|
994
996
|
* `question` (string, required): **Rich Content.** Question text.
|
|
995
|
-
* `
|
|
996
|
-
* `
|
|
997
|
-
* `tolerance` (number, optional): For purely numeric answers, allowed numeric deviation.
|
|
998
|
-
* **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']`.
|
|
997
|
+
* `correctAnswers` (string[], required, at least 1): Correct answers as raw LaTeX **without** `$` / `$$` delimiters (e.g., `["2x + 2", "2*x + 2", "2+2x"]`); all are equally correct, as the `correctAnswers` of `short-answer`. Values wrapped in `$` MUST cause a validation error.
|
|
998
|
+
* **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 the string with each item of `correctAnswers`. (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 `correctAnswers`. Example: `correctAnswers: ['2x+2', '2*x+2', '2+2x']`.
|
|
999
999
|
|
|
1000
1000
|
**Example:**
|
|
1001
1001
|
|
|
@@ -1005,8 +1005,8 @@ In the example above, `BE-4` has no connection: it is a distractor.
|
|
|
1005
1005
|
"type": "math-input",
|
|
1006
1006
|
"difficulty": 4,
|
|
1007
1007
|
"question": "Simplify the following expression: $2(x+1)$",
|
|
1008
|
-
"
|
|
1009
|
-
|
|
1008
|
+
"correctAnswers": [
|
|
1009
|
+
"2x + 2",
|
|
1010
1010
|
"2*x + 2"
|
|
1011
1011
|
]
|
|
1012
1012
|
}
|
|
@@ -1203,23 +1203,24 @@ Defines clickable zone on image or named mesh in a 3D model. For `pin-on-image`,
|
|
|
1203
1203
|
|
|
1204
1204
|
**For `type: "rect"` (rectangle):**
|
|
1205
1205
|
|
|
1206
|
-
* `x` (number): Percent X from left edge.
|
|
1207
|
-
* `y` (number): Percent Y from top edge.
|
|
1208
|
-
* `width` (number): Width in percentages.
|
|
1209
|
-
* `height` (number): Height in percentages.
|
|
1206
|
+
* `x` (number, required): Percent X from left edge.
|
|
1207
|
+
* `y` (number, required): Percent Y from top edge.
|
|
1208
|
+
* `width` (number, required): Width in percentages.
|
|
1209
|
+
* `height` (number, required): Height in percentages.
|
|
1210
1210
|
|
|
1211
1211
|
**For `type: "circle"` (circle):**
|
|
1212
1212
|
|
|
1213
|
-
* `x` (number): Center X in percentages.
|
|
1214
|
-
* `y` (number): Center Y in percentages.
|
|
1215
|
-
* `radius` (number): Radius in percentages.
|
|
1213
|
+
* `x` (number, required): Center X in percentages.
|
|
1214
|
+
* `y` (number, required): Center Y in percentages.
|
|
1215
|
+
* `radius` (number, required): Radius in percentages.
|
|
1216
1216
|
|
|
1217
1217
|
**For `type: "polygon"` (polygon):**
|
|
1218
1218
|
|
|
1219
|
-
* `points` (object[]): Array of points defining shape. Each point is `{ "x": number, "y": number }
|
|
1219
|
+
* `points` (object[], required, at least 3): Array of points defining shape. Each point is `{ "x": number, "y": number }` (both required).
|
|
1220
1220
|
|
|
1221
1221
|
**For `type: "mesh"` (3D model part):**
|
|
1222
|
-
|
|
1222
|
+
|
|
1223
|
+
* `targetName` (string, required): **Plain Text.** Name (or partial name) of the object/mesh in the 3D scene (glTF node name).
|
|
1223
1224
|
|
|
1224
1225
|
### `CategorizeEntry` (for `categorize`)
|
|
1225
1226
|
|
|
@@ -1472,8 +1473,8 @@ This optional object (`item.pedagogy`) serves to store advanced metadata about d
|
|
|
1472
1473
|
"tags": ["Rocket Engine"],
|
|
1473
1474
|
"difficulty": 3,
|
|
1474
1475
|
"question": "Match countries with their main spaceports (some items are extra):",
|
|
1475
|
-
"
|
|
1476
|
-
"
|
|
1476
|
+
"prompts": [{ "id": "usa", "text": "USA" }, { "id": "france", "text": "France" }, { "id": "kz", "text": "Kazakhstan" }],
|
|
1477
|
+
"matches": [{ "id": "cape", "text": "Cape Canaveral" }, { "id": "kourou", "text": "Kourou" }, { "id": "baikonur", "text": "Baikonur" }, { "id": "cornwall", "text": "Spaceport Cornwall" }],
|
|
1477
1478
|
"connections": [
|
|
1478
1479
|
["usa", "cape"],
|
|
1479
1480
|
["france", "kourou"],
|
|
@@ -1531,6 +1532,15 @@ This optional object (`item.pedagogy`) serves to store advanced metadata about d
|
|
|
1531
1532
|
|
|
1532
1533
|
-----
|
|
1533
1534
|
|
|
1535
|
+
## Canonical and Authoring Formats
|
|
1536
|
+
|
|
1537
|
+
The JSON file described in this document is the **canonical** form of a study set. Applications store it, exchange it and validate it; item IDs, learning progress and statistics refer to it.
|
|
1538
|
+
|
|
1539
|
+
People and AI assistants may write sets in **authoring formats** that are easier to write – today [OQSE Markdown](#oqse-markdown-note-sets) for notes; tools may offer others (e.g., a shorthand with options as plain texts). An authoring format is only an input: a tool converts it to the canonical form, and applications are not required to read it. Two rules apply to every authoring format:
|
|
1540
|
+
|
|
1541
|
+
1. **The canonical form is what is stored and exchanged.** An authoring file is never a substitute for it.
|
|
1542
|
+
2. **Item IDs survive edits.** Progress and statistics are bound to item IDs (and answers to choice IDs). Converting an edited source again MUST keep the IDs of existing items and choices: the source keeps them (as OQSE Markdown does in its metadata comments), or the authoring format is used once and the canonical file is edited afterwards.
|
|
1543
|
+
|
|
1534
1544
|
## OQSE Markdown (Note Sets)
|
|
1535
1545
|
|
|
1536
1546
|
A set that contains only `note` items MAY be written as a single Markdown document (RECOMMENDED extension `.oqse.md`, UTF-8). Each note is a section under a heading. The representation is **losslessly equivalent** to the JSON file (see the normalization rules below) and exists so that notes with complex content (Mermaid diagrams, LaTeX, code) can be written and edited without escaping them inside JSON strings. It follows the common Markdown convention of a single top-level heading (as recommended by markdownlint and GitHub) and works well in editors such as Obsidian.
|
|
@@ -2211,8 +2221,11 @@ OQSE is an open standard. Suggestions for improvements:
|
|
|
2211
2221
|
|
|
2212
2222
|
-----
|
|
2213
2223
|
|
|
2214
|
-
### Version 1.0 (
|
|
2224
|
+
### Version 1.0 (2026-10-14)
|
|
2215
2225
|
The first stable version. From now on, 1.x versions only add (see [Versioning and Compatibility](#versioning-and-compatibility)).
|
|
2226
|
+
* The shape fields of a `HotspotObject` (`x`, `y`, `width`, `height`, `radius`, `points`, `targetName`) are marked required, as validators always treated them.
|
|
2227
|
+
* Before 1.0, two item types were aligned with the others: `math-input` has `correctAnswers` (a list, like `short-answer`) instead of `correctAnswer` + `alternativeAnswers`, and `match-complex` has `prompts` / `matches` (like `match-pairs`) instead of `leftItems` / `rightItems`. `math-input` has no `tolerance` (its answers are compared as LaTeX; numbers are `numeric-input`).
|
|
2228
|
+
* [Canonical and Authoring Formats](#canonical-and-authoring-formats): the JSON file is the canonical form that is stored and exchanged; authoring formats (OQSE Markdown for notes, tool-specific shorthands) are converted to it and keep item IDs across edits.
|
|
2216
2229
|
* Version rules for sets, manifests and progress files: applications read the MAJOR versions they support and never read another MAJOR version (including the pre-release 0.x drafts) silently as a supported one.
|
|
2217
2230
|
* Schema URLs: `https://cdn.jsdelivr.net/npm/@memizy/oqse@1/schemas/oqse-v1.0.json` (and `oqse-manifest-v1.0.json`, `oqse-progress-v1.0.json`, `oqse-header-v1.0.json`).
|
|
2218
2231
|
* Markdown images `` are explicitly allowed for pictures from the web (absolute URL; `<asset:key />` for everything else). Blank tags belong only in the `text` of `fill-in-blanks` and `fill-in-select` (elsewhere a warning; shown as an empty line).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memizy/oqse",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.6",
|
|
4
4
|
"description": "TypeScript SDK and Zod validators for the OQSE specification.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
"oqse-manifest.md",
|
|
23
23
|
"oqse-progress.md",
|
|
24
24
|
"oqse-header.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
25
26
|
"LICENSE"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
28
29
|
"dev": "vite build --watch",
|
|
29
30
|
"generate:schemas": "tsx scripts/generate-schemas.ts",
|
|
30
|
-
"build": "npm run generate:schemas && tsc && vite build && tsx scripts/fix-types-entry.ts",
|
|
31
|
+
"build": "npm run generate:schemas && tsc && vite build && tsx scripts/fix-types-entry.ts && node ../../scripts/dts-extensions.mjs dist",
|
|
31
32
|
"test": "vitest run",
|
|
32
33
|
"test:watch": "vitest",
|
|
33
34
|
"prepublishOnly": "npm run build",
|
|
@@ -47,6 +48,17 @@
|
|
|
47
48
|
"yaml": "^2.9.1",
|
|
48
49
|
"zod": "^4.3.6"
|
|
49
50
|
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"author": "Memizy Team",
|
|
53
|
+
"keywords": [
|
|
54
|
+
"oqse",
|
|
55
|
+
"memizy",
|
|
56
|
+
"study-set",
|
|
57
|
+
"quiz",
|
|
58
|
+
"flashcards",
|
|
59
|
+
"education",
|
|
60
|
+
"zod"
|
|
61
|
+
],
|
|
50
62
|
"repository": {
|
|
51
63
|
"type": "git",
|
|
52
64
|
"url": "git+https://github.com/memizy/memizy.git",
|
package/schemas/oqse-v1.0.json
CHANGED
|
@@ -3638,7 +3638,7 @@
|
|
|
3638
3638
|
"type": "string",
|
|
3639
3639
|
"maxLength": 10000
|
|
3640
3640
|
},
|
|
3641
|
-
"
|
|
3641
|
+
"prompts": {
|
|
3642
3642
|
"minItems": 1,
|
|
3643
3643
|
"maxItems": 100,
|
|
3644
3644
|
"type": "array",
|
|
@@ -3667,7 +3667,7 @@
|
|
|
3667
3667
|
"additionalProperties": {}
|
|
3668
3668
|
}
|
|
3669
3669
|
},
|
|
3670
|
-
"
|
|
3670
|
+
"matches": {
|
|
3671
3671
|
"minItems": 1,
|
|
3672
3672
|
"maxItems": 100,
|
|
3673
3673
|
"type": "array",
|
|
@@ -3725,8 +3725,8 @@
|
|
|
3725
3725
|
"required": [
|
|
3726
3726
|
"id",
|
|
3727
3727
|
"type",
|
|
3728
|
-
"
|
|
3729
|
-
"
|
|
3728
|
+
"prompts",
|
|
3729
|
+
"matches",
|
|
3730
3730
|
"connections"
|
|
3731
3731
|
],
|
|
3732
3732
|
"additionalProperties": {}
|
|
@@ -6133,25 +6133,19 @@
|
|
|
6133
6133
|
"minLength": 1,
|
|
6134
6134
|
"maxLength": 10000
|
|
6135
6135
|
},
|
|
6136
|
-
"
|
|
6137
|
-
"
|
|
6138
|
-
},
|
|
6139
|
-
"alternativeAnswers": {
|
|
6136
|
+
"correctAnswers": {
|
|
6137
|
+
"minItems": 1,
|
|
6140
6138
|
"type": "array",
|
|
6141
6139
|
"items": {
|
|
6142
6140
|
"type": "string"
|
|
6143
6141
|
}
|
|
6144
|
-
},
|
|
6145
|
-
"tolerance": {
|
|
6146
|
-
"type": "number",
|
|
6147
|
-
"minimum": 0
|
|
6148
6142
|
}
|
|
6149
6143
|
},
|
|
6150
6144
|
"required": [
|
|
6151
6145
|
"id",
|
|
6152
6146
|
"type",
|
|
6153
6147
|
"question",
|
|
6154
|
-
"
|
|
6148
|
+
"correctAnswers"
|
|
6155
6149
|
],
|
|
6156
6150
|
"additionalProperties": {}
|
|
6157
6151
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/utils.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|