@intellectif/lk-core 0.2.1 → 0.3.1

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.
Files changed (59) hide show
  1. package/dist/activity-zdcAMtFB.d.cts +626 -0
  2. package/dist/activity-zdcAMtFB.d.ts +626 -0
  3. package/dist/chunk-3YAVDV5F.js +47 -0
  4. package/dist/chunk-3YAVDV5F.js.map +1 -0
  5. package/dist/chunk-55O4M45K.js +57 -0
  6. package/dist/chunk-55O4M45K.js.map +1 -0
  7. package/dist/chunk-5GJJHGY5.js +576 -0
  8. package/dist/chunk-5GJJHGY5.js.map +1 -0
  9. package/dist/{chunk-ZHOX6TO4.js → chunk-HS7BYCGE.js} +65 -7
  10. package/dist/chunk-HS7BYCGE.js.map +1 -0
  11. package/dist/chunk-PIMX4B4D.cjs +47 -0
  12. package/dist/chunk-PIMX4B4D.cjs.map +1 -0
  13. package/dist/chunk-QOAORTA4.cjs +576 -0
  14. package/dist/chunk-QOAORTA4.cjs.map +1 -0
  15. package/dist/chunk-QSVBYTNM.js +94 -0
  16. package/dist/chunk-QSVBYTNM.js.map +1 -0
  17. package/dist/{chunk-PJFDAP54.cjs → chunk-RUGIOQZY.cjs} +66 -8
  18. package/dist/chunk-RUGIOQZY.cjs.map +1 -0
  19. package/dist/chunk-THSZMZND.cjs +94 -0
  20. package/dist/chunk-THSZMZND.cjs.map +1 -0
  21. package/dist/chunk-YNTYWHZI.cjs +57 -0
  22. package/dist/chunk-YNTYWHZI.cjs.map +1 -0
  23. package/dist/index.cjs +185 -6
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +253 -7
  26. package/dist/index.d.ts +253 -7
  27. package/dist/index.js +188 -9
  28. package/dist/index.js.map +1 -1
  29. package/dist/schemas.cjs +29 -2
  30. package/dist/schemas.cjs.map +1 -1
  31. package/dist/schemas.d.cts +310 -29
  32. package/dist/schemas.d.ts +310 -29
  33. package/dist/schemas.js +32 -5
  34. package/dist/scoring.cjs +13 -3
  35. package/dist/scoring.cjs.map +1 -1
  36. package/dist/scoring.d.cts +29 -5
  37. package/dist/scoring.d.ts +29 -5
  38. package/dist/scoring.js +12 -2
  39. package/dist/xapi.cjs +3 -3
  40. package/dist/xapi.d.cts +40 -2
  41. package/dist/xapi.d.ts +40 -2
  42. package/dist/xapi.js +2 -2
  43. package/package.json +7 -2
  44. package/dist/activity-D5tqgx8A.d.cts +0 -330
  45. package/dist/activity-D5tqgx8A.d.ts +0 -330
  46. package/dist/chunk-2T3IL7VL.js +0 -147
  47. package/dist/chunk-2T3IL7VL.js.map +0 -1
  48. package/dist/chunk-3JMLUUFS.cjs +0 -124
  49. package/dist/chunk-3JMLUUFS.cjs.map +0 -1
  50. package/dist/chunk-6VOCV4EX.js +0 -25
  51. package/dist/chunk-6VOCV4EX.js.map +0 -1
  52. package/dist/chunk-DCDOTQNH.cjs +0 -147
  53. package/dist/chunk-DCDOTQNH.cjs.map +0 -1
  54. package/dist/chunk-DV466SBJ.cjs +0 -25
  55. package/dist/chunk-DV466SBJ.cjs.map +0 -1
  56. package/dist/chunk-GZ73G2TS.js +0 -124
  57. package/dist/chunk-GZ73G2TS.js.map +0 -1
  58. package/dist/chunk-PJFDAP54.cjs.map +0 -1
  59. package/dist/chunk-ZHOX6TO4.js.map +0 -1
@@ -0,0 +1,57 @@
1
+ import {
2
+ FillInTheBlanksDataSchema,
3
+ MultipleChoiceDataSchema,
4
+ WrittenResponseDataSchema,
5
+ getActivityTypeDescriptor
6
+ } from "./chunk-5GJJHGY5.js";
7
+ import {
8
+ UnknownActivityTypeError
9
+ } from "./chunk-3YAVDV5F.js";
10
+
11
+ // src/schemas/json-schema.ts
12
+ import { z } from "zod/v4";
13
+ var multipleChoiceJsonSchema = z.toJSONSchema(MultipleChoiceDataSchema, {
14
+ target: "draft-7"
15
+ });
16
+ var fillInTheBlanksJsonSchema = z.toJSONSchema(FillInTheBlanksDataSchema, {
17
+ target: "draft-7"
18
+ });
19
+ var writtenResponseJsonSchema = z.toJSONSchema(WrittenResponseDataSchema, {
20
+ target: "draft-7"
21
+ });
22
+ function jsonSchemaFor(type) {
23
+ const descriptor = getActivityTypeDescriptor(type);
24
+ if (descriptor === void 0) {
25
+ throw new UnknownActivityTypeError(type);
26
+ }
27
+ return z.toJSONSchema(descriptor.schema, { target: "draft-7" });
28
+ }
29
+
30
+ // src/schemas/index.ts
31
+ function validateActivity(type, data) {
32
+ const descriptor = getActivityTypeDescriptor(type);
33
+ if (descriptor === void 0) {
34
+ throw new UnknownActivityTypeError(String(type));
35
+ }
36
+ const result = descriptor.schema.safeParse(data);
37
+ if (result.success) {
38
+ return { success: true, data: result.data };
39
+ }
40
+ return {
41
+ success: false,
42
+ errors: result.error.issues.map((issue) => ({
43
+ path: issue.path.map(String),
44
+ message: issue.message,
45
+ code: issue.code
46
+ }))
47
+ };
48
+ }
49
+
50
+ export {
51
+ multipleChoiceJsonSchema,
52
+ fillInTheBlanksJsonSchema,
53
+ writtenResponseJsonSchema,
54
+ jsonSchemaFor,
55
+ validateActivity
56
+ };
57
+ //# sourceMappingURL=chunk-55O4M45K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schemas/json-schema.ts","../src/schemas/index.ts"],"sourcesContent":["import { z } from 'zod/v4';\nimport { UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport { FillInTheBlanksDataSchema } from './fill-in-the-blanks.js';\nimport { MultipleChoiceDataSchema } from './multiple-choice.js';\nimport { WrittenResponseDataSchema } from './written-response.js';\n\n/**\n * JSON Schema (Draft 7) representation of the Multiple Choice activity data\n * contract, generated natively by Zod 4. Draft 7 is mandated by Requirement\n * 2.2 for the widest AI-prompt / OpenAPI tooling compatibility. The semantic\n * `.refine()` guards are not representable in JSON Schema and are\n * intentionally omitted — the export captures the *structural* contract only.\n * Since v0.3 the source schemas are loose, so these no longer emit\n * `additionalProperties: false` — the JSON Schema and `validateActivity` now\n * agree on unknown-key handling.\n */\nexport const multipleChoiceJsonSchema = z.toJSONSchema(MultipleChoiceDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * JSON Schema (Draft 7) representation of the Fill-in-the-Blanks activity data\n * contract, generated natively by Zod 4. Structural contract only (semantic\n * `.refine()` guards are Zod-only and not representable in JSON Schema).\n */\nexport const fillInTheBlanksJsonSchema = z.toJSONSchema(FillInTheBlanksDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * JSON Schema (Draft 7) representation of the Written Response activity data\n * contract. Structural contract only.\n */\nexport const writtenResponseJsonSchema = z.toJSONSchema(WrittenResponseDataSchema, {\n target: 'draft-7',\n});\n\n/**\n * Derives the JSON Schema (Draft 7) for any REGISTERED activity type — the\n * live, registry-backed replacement for the static per-type exports above,\n * and the building block for AI generation pipelines (R6.1): pass the result\n * as a structured-output schema so a model can only emit valid items.\n *\n * @throws UnknownActivityTypeError when `type` has no registered descriptor.\n */\nexport function jsonSchemaFor(type: string): Record<string, unknown> {\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(type);\n }\n return z.toJSONSchema(descriptor.schema as never, { target: 'draft-7' }) as Record<\n string,\n unknown\n >;\n}\n","import { UnknownActivityTypeError } from '../errors.js';\nimport { getActivityTypeDescriptor } from '../registry/index.js';\nimport type { ActivityDataMap, ActivityType, ValidationResult } from '../types/activity.js';\n\nexport { FeedbackSchema } from './feedback.js';\nexport {\n BlankConfigSchema,\n FillInTheBlanksDataSchema,\n TextMatchPolicySchema,\n} from './fill-in-the-blanks.js';\nexport {\n fillInTheBlanksJsonSchema,\n jsonSchemaFor,\n multipleChoiceJsonSchema,\n writtenResponseJsonSchema,\n} from './json-schema.js';\nexport { MediaSchema, MediaUrlSchema } from './media.js';\nexport { MultipleChoiceDataSchema, MultipleChoiceOptionSchema } from './multiple-choice.js';\nexport {\n RedactedBlankConfigSchema,\n RedactedFillInTheBlanksDataSchema,\n RedactedMultipleChoiceDataSchema,\n RedactedMultipleChoiceOptionSchema,\n RedactedWrittenResponseDataSchema,\n} from './redacted.js';\nexport {\n WrittenResponseDataSchema,\n WrittenResponseRubricCriterionSchema,\n WrittenResponseRubricSchema,\n} from './written-response.js';\n\n/**\n * Validates raw activity data against the schema registered for the given\n * activity type (built-in or consumer-registered via `registerActivityType`).\n *\n * Unknown keys are PRESERVED, not stripped: every built-in schema is loose,\n * so consumer sidecar fields and forward-version fields survive validation\n * verbatim in the returned `data`.\n *\n * @returns `{ success: true, data }` with the typed, validated data, or\n * `{ success: false, errors }` with one entry per failed constraint.\n * @throws UnknownActivityTypeError when `type` has no registered descriptor.\n */\nexport function validateActivity<T extends ActivityType>(\n type: T,\n data: unknown,\n): ValidationResult<ActivityDataMap[T]> {\n const descriptor = getActivityTypeDescriptor(type);\n if (descriptor === undefined) {\n throw new UnknownActivityTypeError(String(type));\n }\n\n const result = descriptor.schema.safeParse(data);\n\n if (result.success) {\n return { success: true, data: result.data as ActivityDataMap[T] };\n }\n\n return {\n success: false,\n errors: result.error.issues.map((issue) => ({\n path: issue.path.map(String),\n message: issue.message,\n code: issue.code,\n })),\n };\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,SAAS;AAiBX,IAAM,2BAA2B,EAAE,aAAa,0BAA0B;AAAA,EAC/E,QAAQ;AACV,CAAC;AAOM,IAAM,4BAA4B,EAAE,aAAa,2BAA2B;AAAA,EACjF,QAAQ;AACV,CAAC;AAMM,IAAM,4BAA4B,EAAE,aAAa,2BAA2B;AAAA,EACjF,QAAQ;AACV,CAAC;AAUM,SAAS,cAAc,MAAuC;AACnE,QAAM,aAAa,0BAA0B,IAAI;AACjD,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI,yBAAyB,IAAI;AAAA,EACzC;AACA,SAAO,EAAE,aAAa,WAAW,QAAiB,EAAE,QAAQ,UAAU,CAAC;AAIzE;;;ACZO,SAAS,iBACd,MACA,MACsC;AACtC,QAAM,aAAa,0BAA0B,IAAI;AACjD,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI,yBAAyB,OAAO,IAAI,CAAC;AAAA,EACjD;AAEA,QAAM,SAAS,WAAW,OAAO,UAAU,IAAI;AAE/C,MAAI,OAAO,SAAS;AAClB,WAAO,EAAE,SAAS,MAAM,MAAM,OAAO,KAA2B;AAAA,EAClE;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,QAAQ,OAAO,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,MAC1C,MAAM,MAAM,KAAK,IAAI,MAAM;AAAA,MAC3B,SAAS,MAAM;AAAA,MACf,MAAM,MAAM;AAAA,IACd,EAAE;AAAA,EACJ;AACF;","names":[]}
@@ -0,0 +1,576 @@
1
+ // src/count-words.ts
2
+ function countWords(text) {
3
+ if (typeof text !== "string") {
4
+ return 0;
5
+ }
6
+ const trimmed = text.trim();
7
+ if (trimmed === "") {
8
+ return 0;
9
+ }
10
+ return trimmed.split(/\s+/).length;
11
+ }
12
+
13
+ // src/schemas/feedback.ts
14
+ import { z } from "zod/v4";
15
+ var FeedbackSchema = z.looseObject({
16
+ correct: z.string().min(1).optional(),
17
+ incorrect: z.string().min(1).optional()
18
+ });
19
+
20
+ // src/schemas/media.ts
21
+ import { z as z2 } from "zod/v4";
22
+ var MediaUrlSchema = z2.union([
23
+ z2.url().refine((value) => /^(https?|data|blob):/i.test(value), {
24
+ error: "Absolute media URLs must use the https:, http:, data:, or blob: scheme."
25
+ }),
26
+ z2.string().regex(/^\/(?!\/)\S*$/, {
27
+ error: 'Relative media URLs must be root-relative (a single leading "/").'
28
+ })
29
+ ]);
30
+ var MediaSchema = z2.looseObject({
31
+ type: z2.enum(["image", "audio", "video", "embed"]),
32
+ url: MediaUrlSchema,
33
+ alt: z2.string().min(1).optional(),
34
+ captionsUrl: MediaUrlSchema.optional()
35
+ }).refine(
36
+ (m) => m.type !== "image" && m.type !== "embed" || typeof m.alt === "string" && m.alt.length > 0,
37
+ {
38
+ error: "image and embed media require non-empty alt text (WCAG 1.1.1 / 4.1.2).",
39
+ path: ["alt"]
40
+ }
41
+ ).refine((m) => m.type !== "embed" || /^https?:\/\//i.test(m.url), {
42
+ error: "embed media requires an absolute http(s) provider URL. data:, blob:, and relative URLs are not allowed for embeds \u2014 the embed iframe runs with allow-scripts, and a data:/same-origin document there is an XSS vector.",
43
+ path: ["url"]
44
+ });
45
+
46
+ // src/schemas/fill-in-the-blanks.ts
47
+ import { z as z3 } from "zod/v4";
48
+ var PLACEHOLDER_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
49
+ var TextMatchPolicySchema = z3.looseObject({
50
+ caseSensitive: z3.boolean().optional(),
51
+ trim: z3.boolean().optional(),
52
+ normalize: z3.enum(["none", "NFC", "NFKC"]).optional(),
53
+ foldDiacritics: z3.boolean().optional(),
54
+ collapseInnerWhitespace: z3.boolean().optional(),
55
+ ignorePunctuation: z3.boolean().optional(),
56
+ levenshtein: z3.number().int().min(0).optional(),
57
+ locale: z3.string().optional()
58
+ });
59
+ var BlankConfigSchema = z3.looseObject({
60
+ id: z3.string().min(1),
61
+ acceptedAnswers: z3.array(
62
+ z3.string().min(1).refine((answer) => answer.trim().length > 0, {
63
+ error: "Accepted answers must contain non-whitespace characters."
64
+ })
65
+ ).min(1),
66
+ caseSensitive: z3.boolean().optional(),
67
+ trimWhitespace: z3.boolean().optional(),
68
+ match: TextMatchPolicySchema.optional(),
69
+ hint: z3.string().optional(),
70
+ feedback: z3.string().optional()
71
+ });
72
+ var FillInTheBlanksDataSchema = z3.looseObject({
73
+ schemaVersion: z3.literal("1.0"),
74
+ type: z3.literal("fill-in-the-blanks"),
75
+ id: z3.string().min(1),
76
+ title: z3.string().min(1),
77
+ passage: z3.string().min(1),
78
+ passageHtml: z3.string().optional(),
79
+ blanks: z3.array(BlankConfigSchema).min(1),
80
+ scoringStrategy: z3.enum(["all-or-nothing", "partial"]),
81
+ media: MediaSchema.optional(),
82
+ feedback: FeedbackSchema.optional(),
83
+ passThreshold: z3.number().min(0).max(1).optional(),
84
+ locale: z3.string().optional(),
85
+ learningObjectives: z3.array(z3.string()).optional(),
86
+ difficultyLevel: z3.literal([1, 2, 3, 4, 5]).optional()
87
+ }).refine(
88
+ (data) => {
89
+ const placeholderCounts = /* @__PURE__ */ new Map();
90
+ for (const match of data.passage.matchAll(PLACEHOLDER_RE)) {
91
+ const id = match[1];
92
+ placeholderCounts.set(id, (placeholderCounts.get(id) ?? 0) + 1);
93
+ }
94
+ const blankIds = data.blanks.map((blank) => blank.id);
95
+ if (new Set(blankIds).size !== blankIds.length) {
96
+ return false;
97
+ }
98
+ if (placeholderCounts.size !== blankIds.length) {
99
+ return false;
100
+ }
101
+ return blankIds.every((id) => placeholderCounts.get(id) === 1);
102
+ },
103
+ {
104
+ error: "Each blank id must appear exactly once in blanks[] and have exactly one matching {{id}} placeholder in the passage, and vice versa.",
105
+ path: ["passage"]
106
+ }
107
+ );
108
+
109
+ // src/schemas/multiple-choice.ts
110
+ import { z as z4 } from "zod/v4";
111
+ var MultipleChoiceOptionSchema = z4.looseObject({
112
+ id: z4.string().min(1),
113
+ text: z4.string().min(1),
114
+ isCorrect: z4.boolean(),
115
+ feedback: z4.string().optional()
116
+ });
117
+ var MultipleChoiceDataSchema = z4.looseObject({
118
+ schemaVersion: z4.literal("1.0"),
119
+ type: z4.literal("multiple-choice"),
120
+ id: z4.string().min(1),
121
+ title: z4.string().min(1),
122
+ question: z4.string().min(1),
123
+ questionHtml: z4.string().optional(),
124
+ mode: z4.enum(["single", "multi"]),
125
+ options: z4.array(MultipleChoiceOptionSchema).min(2).max(26),
126
+ scoringStrategy: z4.enum(["all-or-nothing", "partial"]),
127
+ media: MediaSchema.optional(),
128
+ feedback: FeedbackSchema.optional(),
129
+ passThreshold: z4.number().min(0).max(1).optional(),
130
+ shuffle: z4.boolean().optional(),
131
+ locale: z4.string().optional(),
132
+ learningObjectives: z4.array(z4.string()).optional(),
133
+ difficultyLevel: z4.literal([1, 2, 3, 4, 5]).optional()
134
+ }).refine((data) => data.options.some((option) => option.isCorrect), {
135
+ error: "At least one option must be marked correct.",
136
+ path: ["options"]
137
+ }).refine(
138
+ (data) => data.mode !== "single" || data.options.filter((option) => option.isCorrect).length === 1,
139
+ {
140
+ error: 'Single-select activities (mode: "single") must have exactly one correct option.',
141
+ path: ["options"]
142
+ }
143
+ ).refine((data) => new Set(data.options.map((option) => option.id)).size === data.options.length, {
144
+ error: "Option ids must be unique within the activity.",
145
+ path: ["options"]
146
+ });
147
+
148
+ // src/schemas/written-response.ts
149
+ import { z as z5 } from "zod/v4";
150
+ var WrittenResponseRubricCriterionSchema = z5.looseObject({
151
+ name: z5.string().min(1),
152
+ description: z5.string().optional(),
153
+ weight: z5.number().min(0)
154
+ });
155
+ var WrittenResponseRubricSchema = z5.looseObject({
156
+ label: z5.string().optional(),
157
+ criteria: z5.array(WrittenResponseRubricCriterionSchema).min(1)
158
+ });
159
+ var WrittenResponseDataSchema = z5.looseObject({
160
+ schemaVersion: z5.literal("1.0"),
161
+ type: z5.literal("written-response"),
162
+ id: z5.string().min(1),
163
+ title: z5.string().min(1),
164
+ prompt: z5.string(),
165
+ promptHtml: z5.string().optional(),
166
+ minWords: z5.number().int().min(0),
167
+ maxWords: z5.number().int().min(1),
168
+ rubric: WrittenResponseRubricSchema.optional(),
169
+ languageTarget: z5.string().optional(),
170
+ media: MediaSchema.optional(),
171
+ feedback: FeedbackSchema.optional(),
172
+ passThreshold: z5.number().min(0).max(1).optional(),
173
+ locale: z5.string().optional(),
174
+ learningObjectives: z5.array(z5.string()).optional(),
175
+ difficultyLevel: z5.literal([1, 2, 3, 4, 5]).optional()
176
+ }).refine((data) => data.maxWords >= data.minWords, {
177
+ error: "maxWords must be greater than or equal to minWords.",
178
+ path: ["maxWords"]
179
+ });
180
+
181
+ // src/schemas/redacted.ts
182
+ import { z as z6 } from "zod/v4";
183
+ var redactedBase = {
184
+ /** Marker distinguishing a redacted projection from full activity data. */
185
+ redacted: z6.literal(true),
186
+ schemaVersion: z6.literal("1.0"),
187
+ id: z6.string().min(1),
188
+ title: z6.string().min(1),
189
+ media: MediaSchema.optional(),
190
+ passThreshold: z6.number().min(0).max(1).optional(),
191
+ locale: z6.string().optional(),
192
+ learningObjectives: z6.array(z6.string()).optional(),
193
+ difficultyLevel: z6.literal([1, 2, 3, 4, 5]).optional()
194
+ };
195
+ var RedactedMultipleChoiceOptionSchema = z6.strictObject({
196
+ id: z6.string().min(1),
197
+ text: z6.string().min(1)
198
+ });
199
+ var RedactedMultipleChoiceDataSchema = z6.strictObject({
200
+ ...redactedBase,
201
+ type: z6.literal("multiple-choice"),
202
+ question: z6.string().min(1),
203
+ questionHtml: z6.string().optional(),
204
+ mode: z6.enum(["single", "multi"]),
205
+ options: z6.array(RedactedMultipleChoiceOptionSchema).min(2).max(26),
206
+ shuffle: z6.boolean().optional()
207
+ });
208
+ var RedactedBlankConfigSchema = z6.strictObject({
209
+ id: z6.string().min(1),
210
+ hint: z6.string().optional()
211
+ });
212
+ var RedactedFillInTheBlanksDataSchema = z6.strictObject({
213
+ ...redactedBase,
214
+ type: z6.literal("fill-in-the-blanks"),
215
+ passage: z6.string().min(1),
216
+ passageHtml: z6.string().optional(),
217
+ blanks: z6.array(RedactedBlankConfigSchema).min(1)
218
+ });
219
+ var RedactedWrittenResponseDataSchema = z6.strictObject({
220
+ ...redactedBase,
221
+ type: z6.literal("written-response"),
222
+ prompt: z6.string(),
223
+ promptHtml: z6.string().optional(),
224
+ minWords: z6.number().int().min(0),
225
+ maxWords: z6.number().int().min(1),
226
+ rubric: WrittenResponseRubricSchema.optional(),
227
+ languageTarget: z6.string().optional()
228
+ });
229
+
230
+ // src/scoring/text-match.ts
231
+ var COMBINING_MARKS_RE = /\p{M}/gu;
232
+ var PUNCTUATION_RE = /\p{P}/gu;
233
+ function applyBaseline(value, policy) {
234
+ let result = value;
235
+ if (policy.trim !== false) {
236
+ result = result.trim();
237
+ }
238
+ if (policy.caseSensitive !== true) {
239
+ result = policy.locale ? result.toLocaleLowerCase(policy.locale) : result.toLowerCase();
240
+ }
241
+ return result;
242
+ }
243
+ function applyNormalization(value, policy) {
244
+ let result = value;
245
+ if (policy.normalize === "NFC" || policy.normalize === "NFKC") {
246
+ result = result.normalize(policy.normalize);
247
+ }
248
+ if (policy.ignorePunctuation === true) {
249
+ result = result.replace(PUNCTUATION_RE, "");
250
+ }
251
+ if (policy.collapseInnerWhitespace === true) {
252
+ result = result.replace(/(?<=\S)\s+(?=\S)/g, " ");
253
+ if (policy.trim !== false) {
254
+ result = result.trim();
255
+ }
256
+ }
257
+ return result;
258
+ }
259
+ function applyDiacriticFold(value) {
260
+ return value.normalize("NFD").replace(COMBINING_MARKS_RE, "").normalize("NFC");
261
+ }
262
+ function levenshteinDistance(a, b, max) {
263
+ if (a === b) return 0;
264
+ if (Math.abs(a.length - b.length) > max) return max + 1;
265
+ if (a.length === 0) return b.length;
266
+ if (b.length === 0) return a.length;
267
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
268
+ for (let i = 1; i <= a.length; i += 1) {
269
+ const current = [i];
270
+ let rowMin = i;
271
+ for (let j = 1; j <= b.length; j += 1) {
272
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
273
+ const value = Math.min(
274
+ previous[j] + 1,
275
+ current[j - 1] + 1,
276
+ previous[j - 1] + cost
277
+ );
278
+ current.push(value);
279
+ if (value < rowMin) rowMin = value;
280
+ }
281
+ if (rowMin > max) return max + 1;
282
+ previous = current;
283
+ }
284
+ return previous[b.length];
285
+ }
286
+ function matchText(input, accepted, policy = {}) {
287
+ const acceptedList = typeof accepted === "string" ? [accepted] : accepted;
288
+ const baselineInput = applyBaseline(input, policy);
289
+ const baselineAccepted = acceptedList.map((answer) => applyBaseline(answer, policy));
290
+ if (baselineAccepted.some((answer) => answer === baselineInput)) {
291
+ return { matched: true, via: "exact" };
292
+ }
293
+ const usesNormalization = policy.normalize === "NFC" || policy.normalize === "NFKC" || policy.ignorePunctuation === true || policy.collapseInnerWhitespace === true;
294
+ const normalizedInput = usesNormalization ? applyNormalization(baselineInput, policy) : baselineInput;
295
+ const normalizedAccepted = usesNormalization ? baselineAccepted.map((answer) => applyNormalization(answer, policy)) : baselineAccepted;
296
+ if (usesNormalization && normalizedAccepted.some((answer) => answer === normalizedInput)) {
297
+ return { matched: true, via: "normalized" };
298
+ }
299
+ const foldedInput = policy.foldDiacritics === true ? applyDiacriticFold(normalizedInput) : normalizedInput;
300
+ const foldedAccepted = policy.foldDiacritics === true ? normalizedAccepted.map((answer) => applyDiacriticFold(answer)) : normalizedAccepted;
301
+ if (policy.foldDiacritics === true && foldedAccepted.some((answer) => answer === foldedInput)) {
302
+ return { matched: true, via: "folded" };
303
+ }
304
+ const maxDistance = policy.levenshtein ?? 0;
305
+ if (maxDistance > 0 && foldedAccepted.some(
306
+ (answer) => levenshteinDistance(foldedInput, answer, maxDistance) <= maxDistance
307
+ )) {
308
+ return { matched: true, via: "fuzzy" };
309
+ }
310
+ return { matched: false, via: "none" };
311
+ }
312
+
313
+ // src/registry/registry.ts
314
+ var registry = /* @__PURE__ */ new Map();
315
+ function defineActivityType(descriptor) {
316
+ return descriptor;
317
+ }
318
+ function registerActivityType(descriptor) {
319
+ const existing = registry.get(descriptor.type);
320
+ if (existing !== void 0) {
321
+ if (existing === descriptor) {
322
+ return;
323
+ }
324
+ throw new Error(
325
+ `Activity type "${descriptor.type}" is already registered. Registering a different descriptor for an existing type is not allowed.`
326
+ );
327
+ }
328
+ registry.set(descriptor.type, descriptor);
329
+ }
330
+ function getActivityTypeDescriptor(type) {
331
+ return registry.get(type);
332
+ }
333
+ function registeredActivityTypes() {
334
+ return [...registry.keys()];
335
+ }
336
+
337
+ // src/scoring/strategies/all-or-nothing.ts
338
+ function allOrNothingStrategy(correctItems) {
339
+ return correctItems.every((isCorrect) => isCorrect) ? 1 : 0;
340
+ }
341
+
342
+ // src/scoring/strategies/partial.ts
343
+ function partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect) {
344
+ const reward = correctSelected / totalCorrect;
345
+ const penalty = totalIncorrect === 0 ? 0 : incorrectSelected / totalIncorrect;
346
+ return Math.max(0, reward - penalty);
347
+ }
348
+ function partialBlankStrategy(correctBlanks, totalBlanks) {
349
+ return correctBlanks / totalBlanks;
350
+ }
351
+
352
+ // src/scoring/activity-scorers/fill-in-the-blanks.ts
353
+ function policyFor(blank) {
354
+ return {
355
+ ...blank.caseSensitive !== void 0 ? { caseSensitive: blank.caseSensitive } : {},
356
+ ...blank.trimWhitespace !== void 0 ? { trim: blank.trimWhitespace } : {},
357
+ ...blank.match
358
+ };
359
+ }
360
+ function scoreFillInTheBlanks(data, response) {
361
+ const details = [];
362
+ const perBlankCorrect = [];
363
+ for (const blank of data.blanks) {
364
+ const rawInput = response.answers[blank.id];
365
+ const input = typeof rawInput === "string" ? rawInput : "";
366
+ const matched = matchText(input, blank.acceptedAnswers, policyFor(blank)).matched;
367
+ perBlankCorrect.push(matched);
368
+ details.push({
369
+ itemId: blank.id,
370
+ correct: matched,
371
+ outcome: matched ? "correct" : "incorrect",
372
+ learnerResponse: [input],
373
+ correctResponse: [...blank.acceptedAnswers],
374
+ weight: 1
375
+ });
376
+ }
377
+ const correctBlanks = perBlankCorrect.filter(Boolean).length;
378
+ const scoreValue = data.scoringStrategy === "all-or-nothing" ? allOrNothingStrategy(perBlankCorrect) : partialBlankStrategy(correctBlanks, data.blanks.length);
379
+ return { score: scoreValue, maxScore: 1, feedback: null, details };
380
+ }
381
+
382
+ // src/scoring/activity-scorers/multiple-choice.ts
383
+ function scoreMultipleChoice(data, response) {
384
+ const optionById = new Map(data.options.map((option) => [option.id, option]));
385
+ const selected = new Set(response.selectedOptionIds);
386
+ const totalCorrect = data.options.filter((option) => option.isCorrect).length;
387
+ const totalIncorrect = data.options.length - totalCorrect;
388
+ let scoreValue;
389
+ if (data.scoringStrategy === "all-or-nothing") {
390
+ if (data.mode === "single") {
391
+ scoreValue = response.selectedOptionIds.length === 1 && optionById.get(response.selectedOptionIds[0])?.isCorrect === true ? 1 : 0;
392
+ } else {
393
+ const correctIds = data.options.filter((o) => o.isCorrect).map((o) => o.id);
394
+ const allCorrectSelected = correctIds.every((id) => selected.has(id));
395
+ scoreValue = selected.size === correctIds.length && allCorrectSelected ? 1 : 0;
396
+ }
397
+ } else {
398
+ let correctSelected = 0;
399
+ let incorrectSelected = 0;
400
+ for (const id of selected) {
401
+ const option = optionById.get(id);
402
+ if (option?.isCorrect) {
403
+ correctSelected += 1;
404
+ } else {
405
+ incorrectSelected += 1;
406
+ }
407
+ }
408
+ scoreValue = partialStrategy(correctSelected, incorrectSelected, totalCorrect, totalIncorrect);
409
+ }
410
+ const details = data.options.map((option) => {
411
+ const wasSelected = selected.has(option.id);
412
+ const outcome = wasSelected ? option.isCorrect ? "correct" : "incorrect" : option.isCorrect ? "incorrect-omission" : "correct-omission";
413
+ return {
414
+ itemId: option.id,
415
+ correct: wasSelected === option.isCorrect,
416
+ outcome,
417
+ learnerResponse: [wasSelected ? "selected" : "not-selected"],
418
+ correctResponse: [option.isCorrect ? "selected" : "not-selected"],
419
+ weight: 1
420
+ };
421
+ });
422
+ return { score: scoreValue, maxScore: 1, feedback: null, details };
423
+ }
424
+
425
+ // src/registry/builtins.ts
426
+ var SHARED_PUBLIC_FIELDS = {
427
+ schemaVersion: "public",
428
+ type: "public",
429
+ id: "public",
430
+ title: "public",
431
+ media: "public",
432
+ passThreshold: "public",
433
+ locale: "public",
434
+ learningObjectives: "public",
435
+ difficultyLevel: "public"
436
+ };
437
+ var MULTIPLE_CHOICE_FIELD_POLICY = {
438
+ ...SHARED_PUBLIC_FIELDS,
439
+ question: "public",
440
+ questionHtml: "public",
441
+ mode: "public",
442
+ shuffle: "public",
443
+ scoringStrategy: "answer-key",
444
+ feedback: "answer-key",
445
+ options: {
446
+ id: "public",
447
+ text: "public",
448
+ isCorrect: "answer-key",
449
+ feedback: "answer-key"
450
+ }
451
+ };
452
+ var FILL_IN_THE_BLANKS_FIELD_POLICY = {
453
+ ...SHARED_PUBLIC_FIELDS,
454
+ passage: "public",
455
+ passageHtml: "public",
456
+ scoringStrategy: "answer-key",
457
+ feedback: "answer-key",
458
+ blanks: {
459
+ id: "public",
460
+ hint: "public",
461
+ acceptedAnswers: "answer-key",
462
+ caseSensitive: "answer-key",
463
+ trimWhitespace: "answer-key",
464
+ match: "answer-key",
465
+ feedback: "answer-key"
466
+ }
467
+ };
468
+ var WRITTEN_RESPONSE_FIELD_POLICY = {
469
+ ...SHARED_PUBLIC_FIELDS,
470
+ prompt: "public",
471
+ promptHtml: "public",
472
+ minWords: "public",
473
+ maxWords: "public",
474
+ languageTarget: "public",
475
+ feedback: "answer-key",
476
+ // A rubric is a LEARNER affordance, not a grader secret: it tells the
477
+ // learner what they are being graded on, which is pedagogically the point
478
+ // of publishing one. (Classifying it author-only broke real deployments
479
+ // that render a rubric panel during the attempt.) A deployment that wants
480
+ // it hidden can tighten this per call via `redact(data, { policy })`.
481
+ rubric: "public"
482
+ };
483
+ var multipleChoiceType = defineActivityType({
484
+ type: "multiple-choice",
485
+ // zod4 optional outputs are `T | undefined`; the hand-written wire types use
486
+ // exact optionals. Structurally identical at runtime — cast is type-level only.
487
+ schema: MultipleChoiceDataSchema,
488
+ scoring: { kind: "sync", score: scoreMultipleChoice },
489
+ isAnswered: (response) => (response?.selectedOptionIds.length ?? 0) > 0,
490
+ fieldPolicy: MULTIPLE_CHOICE_FIELD_POLICY,
491
+ redactedSchema: RedactedMultipleChoiceDataSchema,
492
+ interop: {
493
+ xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
494
+ xapiInteractionType: "choice",
495
+ correctResponsesPattern: (data) => [
496
+ data.options.filter((option) => option.isCorrect).map((option) => option.id).join("[,]")
497
+ ]
498
+ },
499
+ interactions: ["option-selected", "option-deselected", "submitted"]
500
+ });
501
+ var fillInTheBlanksType = defineActivityType({
502
+ type: "fill-in-the-blanks",
503
+ schema: FillInTheBlanksDataSchema,
504
+ scoring: { kind: "sync", score: scoreFillInTheBlanks },
505
+ isAnswered: (response) => Object.values(response?.answers ?? {}).some((answer) => answer.trim().length > 0),
506
+ fieldPolicy: FILL_IN_THE_BLANKS_FIELD_POLICY,
507
+ redactedSchema: RedactedFillInTheBlanksDataSchema,
508
+ interop: {
509
+ xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
510
+ xapiInteractionType: "fill-in",
511
+ // xAPI fill-in pattern: blank answers joined with "[,]". Only the first
512
+ // accepted answer per blank is emitted (full alternates would explode
513
+ // combinatorially); the complete key lives in the activity data.
514
+ correctResponsesPattern: (data) => [
515
+ data.blanks.map((blank) => blank.acceptedAnswers[0] ?? "").join("[,]")
516
+ ]
517
+ },
518
+ interactions: ["blank-filled", "hint-requested", "submitted"]
519
+ });
520
+ var writtenResponseType = defineActivityType({
521
+ type: "written-response",
522
+ schema: WrittenResponseDataSchema,
523
+ scoring: {
524
+ kind: "deferred",
525
+ reason: "requires_async_grading",
526
+ partial: (data, response) => {
527
+ const wordCount = countWords(response?.text ?? "");
528
+ return {
529
+ withinWordBounds: response !== void 0 && wordCount >= data.minWords && wordCount <= data.maxWords,
530
+ wordCount
531
+ };
532
+ }
533
+ },
534
+ isAnswered: (response) => (response?.text.trim().length ?? 0) > 0,
535
+ fieldPolicy: WRITTEN_RESPONSE_FIELD_POLICY,
536
+ redactedSchema: RedactedWrittenResponseDataSchema,
537
+ interop: {
538
+ xapiActivityTypeIri: "http://adlnet.gov/expapi/activities/cmi.interaction",
539
+ xapiInteractionType: "long-fill-in",
540
+ correctResponsesPattern: () => []
541
+ },
542
+ interactions: ["text-changed", "submitted"]
543
+ });
544
+ registerActivityType(multipleChoiceType);
545
+ registerActivityType(fillInTheBlanksType);
546
+ registerActivityType(writtenResponseType);
547
+
548
+ export {
549
+ countWords,
550
+ FeedbackSchema,
551
+ MediaUrlSchema,
552
+ MediaSchema,
553
+ TextMatchPolicySchema,
554
+ BlankConfigSchema,
555
+ FillInTheBlanksDataSchema,
556
+ MultipleChoiceOptionSchema,
557
+ MultipleChoiceDataSchema,
558
+ WrittenResponseRubricCriterionSchema,
559
+ WrittenResponseRubricSchema,
560
+ WrittenResponseDataSchema,
561
+ RedactedMultipleChoiceOptionSchema,
562
+ RedactedMultipleChoiceDataSchema,
563
+ RedactedBlankConfigSchema,
564
+ RedactedFillInTheBlanksDataSchema,
565
+ RedactedWrittenResponseDataSchema,
566
+ levenshteinDistance,
567
+ matchText,
568
+ defineActivityType,
569
+ registerActivityType,
570
+ getActivityTypeDescriptor,
571
+ registeredActivityTypes,
572
+ multipleChoiceType,
573
+ fillInTheBlanksType,
574
+ writtenResponseType
575
+ };
576
+ //# sourceMappingURL=chunk-5GJJHGY5.js.map