@conform-ed/qti-react 0.0.18 → 0.0.20

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/dist/headless.js CHANGED
@@ -3053,10 +3053,1941 @@ var referenceInteractionKinds = [
3053
3053
  "textEntryInteraction",
3054
3054
  "uploadInteraction"
3055
3055
  ];
3056
+ // src/item-score.ts
3057
+ function numericOutcome(value) {
3058
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
3059
+ }
3060
+ function effectiveItemScore(scores, outcomes) {
3061
+ const scoreOutcome = numericOutcome(outcomes["SCORE"]);
3062
+ const maxOutcome = numericOutcome(outcomes["MAXSCORE"]);
3063
+ const summedMax = scores.reduce((total, score) => total + score.maxScore, 0);
3064
+ if (scoreOutcome !== null) {
3065
+ return { raw: scoreOutcome, max: maxOutcome ?? summedMax, fromOutcomes: true };
3066
+ }
3067
+ return {
3068
+ raw: scores.reduce((total, score) => total + score.score, 0),
3069
+ max: maxOutcome ?? summedMax,
3070
+ fromOutcomes: false
3071
+ };
3072
+ }
3073
+ // src/response-validity.ts
3074
+ var countConstraintKinds = ["minChoices", "maxChoices", "minAssociations", "maxAssociations", "minStrings"];
3075
+ function collectInteractionConstraints(content) {
3076
+ const constraints = [];
3077
+ function walk(node) {
3078
+ const record = node;
3079
+ if (isInteractionKind(v0ContentModel, node.kind) && typeof record["responseIdentifier"] === "string") {
3080
+ const responseIdentifier = record["responseIdentifier"];
3081
+ for (const kind of [...countConstraintKinds, "minPlays"]) {
3082
+ const bound = record[kind];
3083
+ if (typeof bound === "number" && bound > 0) {
3084
+ constraints.push({ responseIdentifier, kind, bound });
3085
+ }
3086
+ }
3087
+ const patternMask = record["patternMask"];
3088
+ if (typeof patternMask === "string" && patternMask !== "") {
3089
+ constraints.push({ responseIdentifier, kind: "patternMask", bound: patternMask });
3090
+ }
3091
+ return;
3092
+ }
3093
+ for (const key of ["content", "children"]) {
3094
+ const nested = record[key];
3095
+ if (Array.isArray(nested)) {
3096
+ for (const child of nested) {
3097
+ walk(child);
3098
+ }
3099
+ }
3100
+ }
3101
+ }
3102
+ for (const node of content ?? []) {
3103
+ walk(node);
3104
+ }
3105
+ return constraints;
3106
+ }
3107
+ function memberCount(value) {
3108
+ if (value === null || value === undefined || value === "") {
3109
+ return 0;
3110
+ }
3111
+ if (Array.isArray(value)) {
3112
+ return value.filter((member) => member !== null && member !== "").length;
3113
+ }
3114
+ return 1;
3115
+ }
3116
+ function stringMembers(value) {
3117
+ if (typeof value === "string") {
3118
+ return value === "" ? [] : [value];
3119
+ }
3120
+ if (Array.isArray(value)) {
3121
+ return value.filter((member) => typeof member === "string" && member !== "");
3122
+ }
3123
+ return [];
3124
+ }
3125
+ function violates(constraint, value) {
3126
+ switch (constraint.kind) {
3127
+ case "minChoices":
3128
+ case "minAssociations":
3129
+ case "minStrings":
3130
+ return memberCount(value) < Number(constraint.bound);
3131
+ case "maxChoices":
3132
+ case "maxAssociations":
3133
+ return memberCount(value) > Number(constraint.bound);
3134
+ case "minPlays": {
3135
+ const plays = typeof value === "number" ? value : 0;
3136
+ return plays < Number(constraint.bound);
3137
+ }
3138
+ case "patternMask": {
3139
+ const members = stringMembers(value);
3140
+ if (members.length === 0) {
3141
+ return false;
3142
+ }
3143
+ try {
3144
+ const matches = HA(String(constraint.bound), { language: "xsd" });
3145
+ return !members.every((member) => matches(member));
3146
+ } catch {
3147
+ return false;
3148
+ }
3149
+ }
3150
+ }
3151
+ }
3152
+ function collectResponseViolations(constraints, responses) {
3153
+ return constraints.filter((constraint) => {
3154
+ const value = responses[constraint.responseIdentifier] ?? null;
3155
+ return !isResponseRecord(value) && violates(constraint, value);
3156
+ });
3157
+ }
3158
+
3159
+ // src/store.ts
3160
+ function createAttemptStore(declarations, initialResponses, options) {
3161
+ const seed = options?.seed ?? Math.floor(Math.random() * 2 ** 31);
3162
+ const templateDeclarations = options?.templateDefaultValues ? applyTemplateDefaultOverrides(options.templateDeclarations ?? [], options.templateDefaultValues) : options?.templateDeclarations ?? [];
3163
+ const templateResult = options?.templateProcessing ? executeTemplateProcessing(options.templateProcessing, {
3164
+ templateDeclarations,
3165
+ responseDeclarations: declarations,
3166
+ seed,
3167
+ customOperators: options.customOperators
3168
+ }) : null;
3169
+ const effectiveDeclarations = templateResult ? applyCorrectResponseOverrides(declarations, templateResult.correctResponseOverrides) : declarations;
3170
+ const declarationsById = new Map(effectiveDeclarations.map((declaration) => [declaration.identifier, declaration]));
3171
+ const listeners = new Set;
3172
+ const responseCollectors = new Map;
3173
+ const rpRandom = mulberry32((seed ^ 2654435769) >>> 0);
3174
+ const now = options?.now ?? Date.now;
3175
+ let activeMs = 0;
3176
+ let runningSinceMs = now();
3177
+ const activeSeconds = () => (activeMs + (runningSinceMs === null ? 0 : now() - runningSinceMs)) / 1000;
3178
+ const completionStatusDeclared = (options?.outcomeDeclarations ?? []).some((declaration) => declaration.identifier === "completionStatus");
3179
+ const maintainedOutcomes = () => completionStatusDeclared ? {} : { completionStatus: "unknown" };
3180
+ const violationsOf = (responses) => options?.constraints ? collectResponseViolations(options.constraints, responses) : [];
3181
+ const correctResponses = {};
3182
+ for (const declaration of effectiveDeclarations) {
3183
+ const values = declaration.correctResponse?.values;
3184
+ if (values !== undefined) {
3185
+ correctResponses[declaration.identifier] = declaration.cardinality === "single" ? values[0]?.value ?? null : values.map((entry) => entry.value);
3186
+ }
3187
+ }
3188
+ let snapshot = {
3189
+ responses: { ...initialResponses },
3190
+ submitted: false,
3191
+ scores: [],
3192
+ outcomes: maintainedOutcomes(),
3193
+ templateValues: templateResult?.templateValues ?? {},
3194
+ attemptCount: 0,
3195
+ durationSeconds: null,
3196
+ responseViolations: violationsOf(initialResponses),
3197
+ correctResponses
3198
+ };
3199
+ function emit(next) {
3200
+ snapshot = next;
3201
+ for (const listener of listeners) {
3202
+ listener();
3203
+ }
3204
+ }
3205
+ function computeScores(responses) {
3206
+ return [...declarationsById.values()].map((declaration) => scoreResponse(declaration, responses[declaration.identifier] ?? null, options?.normalization));
3207
+ }
3208
+ function computeOutcomes(responses, durationSeconds, priorOutcomes) {
3209
+ if (!options?.responseProcessing) {
3210
+ return {};
3211
+ }
3212
+ return executeResponseProcessing(options.responseProcessing, {
3213
+ responseDeclarations: effectiveDeclarations,
3214
+ outcomeDeclarations: options.outcomeDeclarations ?? [],
3215
+ responses,
3216
+ normalization: options.normalization,
3217
+ templateDeclarations,
3218
+ templateValues: snapshot.templateValues,
3219
+ priorOutcomes,
3220
+ random: rpRandom,
3221
+ customOperators: options.customOperators,
3222
+ duration: durationSeconds,
3223
+ numAttempts: snapshot.attemptCount + 1,
3224
+ ...typeof snapshot.outcomes["completionStatus"] === "string" ? { completionStatus: snapshot.outcomes["completionStatus"] } : {}
3225
+ }).outcomes;
3226
+ }
3227
+ return {
3228
+ getSnapshot: () => snapshot,
3229
+ subscribe: (listener) => {
3230
+ listeners.add(listener);
3231
+ return () => {
3232
+ listeners.delete(listener);
3233
+ };
3234
+ },
3235
+ setResponse: (responseIdentifier, value) => {
3236
+ if (snapshot.submitted) {
3237
+ return;
3238
+ }
3239
+ const responses = { ...snapshot.responses, [responseIdentifier]: value };
3240
+ emit({
3241
+ ...snapshot,
3242
+ responses,
3243
+ responseViolations: violationsOf(responses)
3244
+ });
3245
+ },
3246
+ registerResponseCollector: (responseIdentifier, collector) => {
3247
+ responseCollectors.set(responseIdentifier, collector);
3248
+ return () => {
3249
+ if (responseCollectors.get(responseIdentifier) === collector) {
3250
+ responseCollectors.delete(responseIdentifier);
3251
+ }
3252
+ };
3253
+ },
3254
+ submit: () => {
3255
+ if (snapshot.submitted) {
3256
+ return snapshot.scores;
3257
+ }
3258
+ let collected = snapshot.responses;
3259
+ for (const [responseIdentifier, collector] of responseCollectors) {
3260
+ const value = collector();
3261
+ if (value !== undefined) {
3262
+ collected = { ...collected, [responseIdentifier]: value };
3263
+ }
3264
+ }
3265
+ if (collected !== snapshot.responses) {
3266
+ snapshot = { ...snapshot, responses: collected, responseViolations: violationsOf(collected) };
3267
+ }
3268
+ if (options?.validateResponses && snapshot.responseViolations.length > 0) {
3269
+ emit(snapshot);
3270
+ return snapshot.scores;
3271
+ }
3272
+ const scores = computeScores(snapshot.responses);
3273
+ const durationSeconds = activeSeconds();
3274
+ const priorOutcomes = options?.adaptive && snapshot.attemptCount > 0 ? snapshot.outcomes : undefined;
3275
+ const rpOutcomes = computeOutcomes(snapshot.responses, durationSeconds, priorOutcomes);
3276
+ const outcomes = options?.responseProcessing ? rpOutcomes : { ...maintainedOutcomes(), ...rpOutcomes };
3277
+ const completionStatus = outcomes["completionStatus"] ?? outcomes["completion_status"];
3278
+ const completed = !options?.adaptive || completionStatus === "completed";
3279
+ let responses = snapshot.responses;
3280
+ if (options?.adaptive && !completed) {
3281
+ responses = { ...responses };
3282
+ for (const declaration of effectiveDeclarations) {
3283
+ if (declaration.baseType === "boolean") {
3284
+ responses = { ...responses, [declaration.identifier]: null };
3285
+ }
3286
+ }
3287
+ }
3288
+ emit({
3289
+ ...snapshot,
3290
+ responses,
3291
+ submitted: completed,
3292
+ scores,
3293
+ outcomes,
3294
+ attemptCount: snapshot.attemptCount + 1,
3295
+ durationSeconds
3296
+ });
3297
+ return scores;
3298
+ },
3299
+ suspend: () => {
3300
+ if (runningSinceMs !== null) {
3301
+ activeMs += now() - runningSinceMs;
3302
+ runningSinceMs = null;
3303
+ }
3304
+ },
3305
+ resume: () => {
3306
+ runningSinceMs ??= now();
3307
+ },
3308
+ reset: () => {
3309
+ activeMs = 0;
3310
+ runningSinceMs = now();
3311
+ emit({
3312
+ responses: { ...initialResponses },
3313
+ submitted: false,
3314
+ scores: [],
3315
+ outcomes: maintainedOutcomes(),
3316
+ templateValues: snapshot.templateValues,
3317
+ attemptCount: 0,
3318
+ durationSeconds: null,
3319
+ responseViolations: violationsOf(initialResponses),
3320
+ correctResponses
3321
+ });
3322
+ }
3323
+ };
3324
+ }
3325
+ // src/pnp.ts
3326
+ var pnpFeatureFields = {
3327
+ "linguistic-guidance": "linguisticGuidance",
3328
+ "keyword-emphasis": "keywordEmphasis",
3329
+ "keyword-translation": "keywordTranslation",
3330
+ "simplified-language-portions": "simplifiedLanguagePortions",
3331
+ "simplified-graphics": "simplifiedGraphics",
3332
+ "item-translation": "itemTranslation",
3333
+ "sign-language": "signLanguage",
3334
+ encouragement: "encouragement",
3335
+ "additional-testing-time": "additionalTestingTime",
3336
+ "line-reader": "lineReader",
3337
+ "invert-display-polarity": "invertDisplayPolarity",
3338
+ magnification: "magnification",
3339
+ spoken: "spoken",
3340
+ tactile: "tactile",
3341
+ braille: "braille",
3342
+ "answer-masking": "answerMasking",
3343
+ "keyboard-directions": "keyboardDirections",
3344
+ "additional-directions": "additionalDirections",
3345
+ "long-description": "longDescription",
3346
+ captions: "captions",
3347
+ transcript: "transcript",
3348
+ "alternative-text": "alternativeText",
3349
+ "audio-description": "audioDescription",
3350
+ "high-contrast": "highContrast",
3351
+ "input-requirements": "inputRequirements",
3352
+ "language-of-interface": "languageOfInterface",
3353
+ "layout-single-column": "layoutSingleColumn",
3354
+ "text-appearance": "textAppearance",
3355
+ "calculator-on-screen": "calculatorOnScreen",
3356
+ "dictionary-on-screen": "dictionaryOnScreen",
3357
+ "glossary-on-screen": "glossaryOnScreen",
3358
+ "thesaurus-on-screen": "thesaurusOnScreen",
3359
+ "homophone-checker-on-screen": "homophoneCheckerOnScreen",
3360
+ "note-taking-on-screen": "noteTakingOnScreen",
3361
+ "visual-organizer-on-screen": "visualOrganizerOnScreen",
3362
+ "outliner-on-screen": "outlinerOnScreen",
3363
+ "peer-interaction-on-screen": "peerInteractionOnScreen",
3364
+ "spell-checker-on-screen": "spellCheckerOnScreen"
3365
+ };
3366
+ function resolvePnpActivation(pnp) {
3367
+ const prohibited = new Set(pnp?.prohibitSet?.features ?? []);
3368
+ const active = new Set;
3369
+ const optional = new Set;
3370
+ if (!pnp) {
3371
+ return { active, optional, prohibited };
3372
+ }
3373
+ const optedIn = new Set(pnp.activateAsOptionSet?.features ?? []);
3374
+ for (const feature of pnp.activateAtInitializationSet?.features ?? []) {
3375
+ active.add(feature);
3376
+ }
3377
+ for (const [feature, field] of Object.entries(pnpFeatureFields)) {
3378
+ if (pnp[field] === undefined || active.has(feature) || optedIn.has(feature)) {
3379
+ continue;
3380
+ }
3381
+ active.add(feature);
3382
+ }
3383
+ for (const feature of optedIn) {
3384
+ if (!active.has(feature)) {
3385
+ optional.add(feature);
3386
+ }
3387
+ }
3388
+ for (const feature of prohibited) {
3389
+ active.delete(feature);
3390
+ optional.delete(feature);
3391
+ }
3392
+ return { active, optional, prohibited };
3393
+ }
3394
+ function languagesMatch(left, right) {
3395
+ const a3 = left.toLowerCase();
3396
+ const b2 = right.toLowerCase();
3397
+ return a3 === b2 || a3.split("-")[0] === b2.split("-")[0];
3398
+ }
3399
+ function camelCase(name) {
3400
+ return name.replace(/-([a-z])/gu, (_2, letter) => letter.toUpperCase());
3401
+ }
3402
+ function pnpFeaturePreference(pnp, feature) {
3403
+ const field = pnpFeatureFields[feature];
3404
+ if (!pnp || !field) {
3405
+ return;
3406
+ }
3407
+ const value = pnp[field];
3408
+ const first = Array.isArray(value) ? value[0] : value;
3409
+ return typeof first === "object" && first !== null ? first : undefined;
3410
+ }
3411
+ function entryMatches(entry, preference) {
3412
+ if (entry.xmlLang !== undefined) {
3413
+ const preferred = preference?.["xmlLang"];
3414
+ if (typeof preferred !== "string" || !languagesMatch(entry.xmlLang, preferred)) {
3415
+ return false;
3416
+ }
3417
+ }
3418
+ for (const [name, value] of Object.entries(entry.dataAttributes ?? {})) {
3419
+ const preferred = preference?.[camelCase(name)];
3420
+ const comparable = typeof preferred === "string" || typeof preferred === "number" || typeof preferred === "boolean" ? `${preferred}` : undefined;
3421
+ if (comparable === undefined || comparable !== value) {
3422
+ return false;
3423
+ }
3424
+ }
3425
+ return true;
3426
+ }
3427
+ function resolveCard(card, pnp) {
3428
+ if (!card.cardEntries) {
3429
+ const cardLang = card.xmlLang ?? card.htmlContent?.xmlLang;
3430
+ return {
3431
+ support: card.support,
3432
+ ...cardLang !== undefined ? { xmlLang: cardLang } : {},
3433
+ ...card.htmlContent?.content ? { content: card.htmlContent.content } : {},
3434
+ ...card.fileHrefs ? { fileHrefs: card.fileHrefs } : {}
3435
+ };
3436
+ }
3437
+ const preference = pnpFeaturePreference(pnp, card.support);
3438
+ const entry = card.cardEntries.find((candidate) => entryMatches(candidate, preference)) ?? card.cardEntries.find((candidate) => candidate.default === true);
3439
+ if (!entry) {
3440
+ return;
3441
+ }
3442
+ const xmlLang = entry.xmlLang ?? entry.htmlContent?.xmlLang ?? card.xmlLang;
3443
+ return {
3444
+ support: card.support,
3445
+ ...xmlLang !== undefined ? { xmlLang } : {},
3446
+ ...entry.htmlContent?.content ? { content: entry.htmlContent.content } : {},
3447
+ ...entry.fileHrefs ? { fileHrefs: entry.fileHrefs } : {}
3448
+ };
3449
+ }
3450
+ function resolveCatalogSupports(options) {
3451
+ const activation = resolvePnpActivation(options.pnp);
3452
+ const effective = new Set(activation.active);
3453
+ for (const support of options.activeSupports ?? []) {
3454
+ if (!activation.prohibited.has(support)) {
3455
+ effective.add(support);
3456
+ }
3457
+ }
3458
+ const byCatalogId = new Map;
3459
+ for (const catalog of options.catalogs ?? []) {
3460
+ const resolved = [];
3461
+ for (const card of catalog.cards) {
3462
+ if (!effective.has(card.support)) {
3463
+ continue;
3464
+ }
3465
+ const support = resolveCard(card, options.pnp);
3466
+ if (support) {
3467
+ resolved.push(support);
3468
+ }
3469
+ }
3470
+ byCatalogId.set(catalog.id, resolved);
3471
+ }
3472
+ return { activation, byCatalogId };
3473
+ }
3474
+
3475
+ // src/test/controller.ts
3476
+ var supportedOutcomeRuleKinds = new Set([
3477
+ "outcomeCondition",
3478
+ "setOutcomeValue",
3479
+ "lookupOutcomeValue",
3480
+ "outcomeProcessingFragment",
3481
+ "exitTest"
3482
+ ]);
3483
+ var testExpressionKinds = new Set([
3484
+ ...deterministicExpressionKinds,
3485
+ "testVariables",
3486
+ "outcomeMinimum",
3487
+ "outcomeMaximum",
3488
+ "numberCorrect",
3489
+ "numberIncorrect",
3490
+ "numberPresented",
3491
+ "numberResponded",
3492
+ "numberSelected"
3493
+ ]);
3494
+
3495
+ class ExitTestSignal extends Error {
3496
+ }
3497
+ function inferBaseType(value) {
3498
+ if (typeof value === "number") {
3499
+ return "float";
3500
+ }
3501
+ if (typeof value === "boolean") {
3502
+ return "boolean";
3503
+ }
3504
+ return;
3505
+ }
3506
+ function liftFlat(value) {
3507
+ if (value === null || value === undefined) {
3508
+ return null;
3509
+ }
3510
+ if (Array.isArray(value)) {
3511
+ return fromFlatValue(value, "multiple", inferBaseType(value[0]));
3512
+ }
3513
+ return fromFlatValue(value, "single", inferBaseType(value));
3514
+ }
3515
+ var specSessionControlDefaults = {
3516
+ maxAttempts: 1,
3517
+ showFeedback: false,
3518
+ allowReview: true,
3519
+ showSolution: false,
3520
+ allowComment: false,
3521
+ allowSkipping: true,
3522
+ validateResponses: false
3523
+ };
3524
+ function definedControl(control) {
3525
+ return control ? Object.fromEntries(Object.entries(control).filter(([, value]) => value !== undefined)) : {};
3526
+ }
3527
+ function seededPick(pool, count, random) {
3528
+ const indices = pool.map((_2, index) => index);
3529
+ for (let i3 = indices.length - 1;i3 > 0; i3 -= 1) {
3530
+ const j2 = Math.floor(random() * (i3 + 1));
3531
+ [indices[i3], indices[j2]] = [indices[j2], indices[i3]];
3532
+ }
3533
+ return indices.slice(0, Math.min(count, indices.length)).sort((a3, b2) => a3 - b2).map((index) => pool[index]);
3534
+ }
3535
+ function applySelection(children, selection, random) {
3536
+ const required = children.filter((child) => child.required === true);
3537
+ const needed = Math.max(0, selection.select - required.length);
3538
+ if (selection.withReplacement === true) {
3539
+ const counts = children.map((child) => child.required === true ? 1 : 0);
3540
+ for (let draw = 0;draw < needed; draw += 1) {
3541
+ const index = Math.floor(random() * children.length);
3542
+ counts[index] = (counts[index] ?? 0) + 1;
3543
+ }
3544
+ return children.flatMap((child, index) => Array.from({ length: counts[index] ?? 0 }, () => child));
3545
+ }
3546
+ const optional = children.filter((child) => child.required !== true);
3547
+ const picked = new Set([...required, ...seededPick(optional, needed, random)]);
3548
+ return children.filter((child) => picked.has(child));
3549
+ }
3550
+ function applyOrdering(units, random) {
3551
+ const result = units.map((unit) => unit.child.fixed === true ? unit : null);
3552
+ const movable = units.filter((unit) => unit.child.fixed !== true);
3553
+ const shuffled = seededPick(movable, movable.length, random);
3554
+ for (let i3 = shuffled.length - 1;i3 > 0; i3 -= 1) {
3555
+ const j2 = Math.floor(random() * (i3 + 1));
3556
+ [shuffled[i3], shuffled[j2]] = [shuffled[j2], shuffled[i3]];
3557
+ }
3558
+ let cursor = 0;
3559
+ return result.map((slot) => slot ?? shuffled[cursor++]);
3560
+ }
3561
+ function mixedUnits(children, random, sections) {
3562
+ return children.flatMap((child) => {
3563
+ if (child.kind !== "assessmentSection" || child.visible !== false || child.keepTogether !== false) {
3564
+ return [{ child, via: [] }];
3565
+ }
3566
+ sections[child.identifier] = {
3567
+ identifier: child.identifier,
3568
+ ...child.timeLimits ? { timeLimits: child.timeLimits } : {}
3569
+ };
3570
+ const inner = child.selection ? applySelection(child.children, child.selection, random) : child.children;
3571
+ return mixedUnits(inner, random, sections).map((unit) => ({ child: unit.child, via: [child, ...unit.via] }));
3572
+ });
3573
+ }
3574
+ function resolveSection(section, partIdentifier, sectionPath, inheritedPreConditions, inheritedControl, random, sections) {
3575
+ const path = [...sectionPath, section.identifier];
3576
+ sections[section.identifier] = {
3577
+ identifier: section.identifier,
3578
+ ...section.timeLimits ? { timeLimits: section.timeLimits } : {}
3579
+ };
3580
+ const preConditions = [...inheritedPreConditions, ...section.preConditions ?? []];
3581
+ const control = { ...inheritedControl, ...definedControl(section.itemSessionControl) };
3582
+ let children = section.children;
3583
+ if (section.selection) {
3584
+ children = applySelection(children, section.selection, random);
3585
+ }
3586
+ let units = children.map((child) => ({ child, via: [] }));
3587
+ if (section.ordering?.shuffle) {
3588
+ units = applyOrdering(mixedUnits(children, random, sections), random);
3589
+ }
3590
+ const items = [];
3591
+ for (const { child, via } of units) {
3592
+ const viaPath = [...path, ...via.map((entry) => entry.identifier)];
3593
+ const viaPreConditions = [...preConditions, ...via.flatMap((entry) => entry.preConditions ?? [])];
3594
+ const viaControl = via.reduce((merged, entry) => ({ ...merged, ...definedControl(entry.itemSessionControl) }), control);
3595
+ if (child.kind === "assessmentSection") {
3596
+ items.push(...resolveSection(child, partIdentifier, viaPath, viaPreConditions, viaControl, random, sections));
3597
+ } else {
3598
+ items.push({
3599
+ key: child.identifier,
3600
+ ref: child,
3601
+ partIdentifier,
3602
+ sectionPath: viaPath,
3603
+ preConditions: [...viaPreConditions, ...child.preConditions ?? []],
3604
+ sessionControl: { ...specSessionControlDefaults, ...viaControl, ...definedControl(child.itemSessionControl) },
3605
+ ...child.timeLimits ? { timeLimits: child.timeLimits } : {}
3606
+ });
3607
+ }
3608
+ }
3609
+ return items;
3610
+ }
3611
+ function resolvePlan(view, seed) {
3612
+ const random = mulberry32(seed);
3613
+ const sections = {};
3614
+ const parts = view.testParts.map((part) => ({
3615
+ identifier: part.identifier,
3616
+ navigationMode: part.navigationMode,
3617
+ submissionMode: part.submissionMode,
3618
+ ...part.timeLimits ? { timeLimits: part.timeLimits } : {},
3619
+ items: part.assessmentSections.flatMap((section) => resolveSection(section, part.identifier, [], [], definedControl(part.itemSessionControl), random, sections))
3620
+ }));
3621
+ const totals = new Map;
3622
+ for (const part of parts) {
3623
+ for (const item of part.items) {
3624
+ totals.set(item.ref.identifier, (totals.get(item.ref.identifier) ?? 0) + 1);
3625
+ }
3626
+ }
3627
+ const ordinals = new Map;
3628
+ const keyedParts = parts.map((part) => ({
3629
+ ...part,
3630
+ items: part.items.map((item) => {
3631
+ if ((totals.get(item.ref.identifier) ?? 0) < 2) {
3632
+ return item;
3633
+ }
3634
+ const instance = (ordinals.get(item.ref.identifier) ?? 0) + 1;
3635
+ ordinals.set(item.ref.identifier, instance);
3636
+ return { ...item, key: `${item.ref.identifier}.${instance}`, instance };
3637
+ })
3638
+ }));
3639
+ return {
3640
+ ...view.timeLimits ? { timeLimits: view.timeLimits } : {},
3641
+ parts: keyedParts,
3642
+ sections
3643
+ };
3644
+ }
3645
+ function createTestController(view, options) {
3646
+ const plan = resolvePlan(view, options.seed);
3647
+ const allItems = plan.parts.flatMap((part) => [...part.items]);
3648
+ const partIndexByItemKey = new Map;
3649
+ const itemsByKey = new Map;
3650
+ const instancesByRef = new Map;
3651
+ plan.parts.forEach((part, partIndex) => {
3652
+ for (const item of part.items) {
3653
+ partIndexByItemKey.set(item.key, partIndex);
3654
+ itemsByKey.set(item.key, item);
3655
+ const siblings = instancesByRef.get(item.ref.identifier);
3656
+ if (siblings) {
3657
+ siblings.push(item);
3658
+ } else {
3659
+ instancesByRef.set(item.ref.identifier, [item]);
3660
+ }
3661
+ }
3662
+ });
3663
+ function attemptsOf(state, itemKey) {
3664
+ return (state.attemptCounts ?? {})[itemKey] ?? 0;
3665
+ }
3666
+ function subsetItems(expression) {
3667
+ const asList2 = (value) => typeof value === "string" ? [value] : value;
3668
+ const includeCategory = asList2(expression.includeCategory);
3669
+ const excludeCategory = asList2(expression.excludeCategory);
3670
+ return allItems.filter((item) => {
3671
+ if (expression.sectionIdentifier !== undefined && !item.sectionPath.includes(expression.sectionIdentifier)) {
3672
+ return false;
3673
+ }
3674
+ const categories = item.ref.categories ?? [];
3675
+ if (includeCategory !== undefined && !includeCategory.some((category) => categories.includes(category))) {
3676
+ return false;
3677
+ }
3678
+ return !(excludeCategory !== undefined && excludeCategory.some((category) => categories.includes(category)));
3679
+ });
3680
+ }
3681
+ function weightOf(item, weightIdentifier) {
3682
+ if (weightIdentifier === undefined) {
3683
+ return 1;
3684
+ }
3685
+ return item.ref.weights?.find((entry) => entry.identifier === weightIdentifier)?.value ?? 1;
3686
+ }
3687
+ function remainingAttempts(state, itemKey) {
3688
+ const item = itemsByKey.get(itemKey);
3689
+ if (!item) {
3690
+ return 0;
3691
+ }
3692
+ const max = item.sessionControl.maxAttempts;
3693
+ return max === 0 ? Number.POSITIVE_INFINITY : Math.max(0, max - attemptsOf(state, itemKey));
3694
+ }
3695
+ const now = options.now ?? Date.now;
3696
+ function touch(state) {
3697
+ if (state.status !== "in-progress") {
3698
+ return state;
3699
+ }
3700
+ const nowMs = now();
3701
+ const timing = state.timing ?? {
3702
+ lastTransitionAtMs: nowMs,
3703
+ testSeconds: 0,
3704
+ partSeconds: {},
3705
+ sectionSeconds: {},
3706
+ itemSeconds: {}
3707
+ };
3708
+ const elapsed = Math.max(0, nowMs - timing.lastTransitionAtMs) / 1000;
3709
+ const bump = (record, key) => ({
3710
+ ...record,
3711
+ [key]: (record[key] ?? 0) + elapsed
3712
+ });
3713
+ const item = state.currentItemKey === null ? undefined : itemsByKey.get(state.currentItemKey);
3714
+ return {
3715
+ ...state,
3716
+ timing: {
3717
+ lastTransitionAtMs: nowMs,
3718
+ testSeconds: timing.testSeconds + elapsed,
3719
+ partSeconds: item ? bump(timing.partSeconds, item.partIdentifier) : timing.partSeconds,
3720
+ sectionSeconds: item ? item.sectionPath.reduce((record, identifier) => bump(record, identifier), timing.sectionSeconds) : timing.sectionSeconds,
3721
+ itemSeconds: item ? bump(timing.itemSeconds, item.key) : timing.itemSeconds
3722
+ }
3723
+ };
3724
+ }
3725
+ function secondsOf(state, scope) {
3726
+ const timing = state.timing;
3727
+ if (!timing) {
3728
+ return 0;
3729
+ }
3730
+ switch (scope.kind) {
3731
+ case "test":
3732
+ return timing.testSeconds;
3733
+ case "part":
3734
+ return timing.partSeconds[scope.identifier] ?? 0;
3735
+ case "section":
3736
+ return timing.sectionSeconds[scope.identifier] ?? 0;
3737
+ case "item":
3738
+ return timing.itemSeconds[scope.key] ?? 0;
3739
+ }
3740
+ }
3741
+ function timeLimitsOf(scope) {
3742
+ switch (scope.kind) {
3743
+ case "test":
3744
+ return plan.timeLimits;
3745
+ case "part":
3746
+ return plan.parts.find((part) => part.identifier === scope.identifier)?.timeLimits;
3747
+ case "section":
3748
+ return plan.sections[scope.identifier]?.timeLimits;
3749
+ case "item":
3750
+ return itemsByKey.get(scope.key)?.timeLimits;
3751
+ }
3752
+ }
3753
+ const additionalTestingTime = resolvePnpActivation(options.pnp).active.has("additional-testing-time") ? options.pnp?.additionalTestingTime : undefined;
3754
+ function effectiveMaxTime(scope) {
3755
+ if (additionalTestingTime?.unlimited === true) {
3756
+ return;
3757
+ }
3758
+ const declared = timeLimitsOf(scope)?.maxTime;
3759
+ if (declared === undefined) {
3760
+ return;
3761
+ }
3762
+ if (additionalTestingTime?.timeMultiplier !== undefined) {
3763
+ return declared * additionalTestingTime.timeMultiplier;
3764
+ }
3765
+ if (additionalTestingTime?.fixedMinutes !== undefined && scope.kind === "test") {
3766
+ return declared + additionalTestingTime.fixedMinutes * 60;
3767
+ }
3768
+ return declared;
3769
+ }
3770
+ function scopeExpired(state, scope) {
3771
+ const maxTime = effectiveMaxTime(scope);
3772
+ return maxTime !== undefined && secondsOf(state, scope) > maxTime;
3773
+ }
3774
+ function enclosingScopes(item) {
3775
+ return [
3776
+ { kind: "item", key: item.key },
3777
+ ...[...item.sectionPath].reverse().map((identifier) => ({ kind: "section", identifier })),
3778
+ { kind: "part", identifier: item.partIdentifier },
3779
+ { kind: "test" }
3780
+ ];
3781
+ }
3782
+ function navigableInTime(state, item) {
3783
+ return !enclosingScopes(item).some((scope) => scopeExpired(state, scope));
3784
+ }
3785
+ function minTimeBlocked(state, from, to) {
3786
+ const itemMin = from.timeLimits?.minTime;
3787
+ if (itemMin !== undefined && secondsOf(state, { kind: "item", key: from.key }) < itemMin) {
3788
+ return true;
3789
+ }
3790
+ const destinationSections = new Set(to?.sectionPath ?? []);
3791
+ return from.sectionPath.some((identifier) => {
3792
+ if (destinationSections.has(identifier)) {
3793
+ return false;
3794
+ }
3795
+ const minTime = plan.sections[identifier]?.timeLimits?.minTime;
3796
+ return minTime !== undefined && secondsOf(state, { kind: "section", identifier }) < minTime;
3797
+ });
3798
+ }
3799
+ function defaultTestOutcomes() {
3800
+ const outcomes = new Map;
3801
+ for (const declaration of view.outcomeDeclarations ?? []) {
3802
+ if (declaration.defaultValue) {
3803
+ outcomes.set(declaration.identifier, rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType));
3804
+ continue;
3805
+ }
3806
+ outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
3807
+ }
3808
+ return outcomes;
3809
+ }
3810
+ const durationValue = (seconds) => rpValue("single", [seconds], "duration");
3811
+ function makeEnv(state, outcomes) {
3812
+ return {
3813
+ lookupVariable: (identifier) => {
3814
+ if (identifier === "duration") {
3815
+ return state.timing === undefined ? null : durationValue(state.timing.testSeconds);
3816
+ }
3817
+ const dot = identifier.indexOf(".");
3818
+ if (dot !== -1) {
3819
+ let itemKey = identifier.slice(0, dot);
3820
+ let variableName = identifier.slice(dot + 1);
3821
+ const secondDot = identifier.indexOf(".", dot + 1);
3822
+ if (secondDot !== -1 && itemsByKey.has(identifier.slice(0, secondDot))) {
3823
+ itemKey = identifier.slice(0, secondDot);
3824
+ variableName = identifier.slice(secondDot + 1);
3825
+ }
3826
+ const instances = instancesByRef.get(itemKey);
3827
+ if (instances !== undefined && instances.length > 1) {
3828
+ const partIndex = partIndexByItemKey.get(instances[0].key);
3829
+ if (partIndex === undefined || plan.parts[partIndex].submissionMode !== "simultaneous") {
3830
+ return null;
3831
+ }
3832
+ for (let index = instances.length - 1;index >= 0; index -= 1) {
3833
+ const instanceKey = instances[index].key;
3834
+ if (variableName === "duration") {
3835
+ const seconds = state.itemDurationSeconds?.[instanceKey];
3836
+ if (seconds !== undefined) {
3837
+ return durationValue(seconds);
3838
+ }
3839
+ } else if (state.itemOutcomes[instanceKey] !== undefined) {
3840
+ return liftFlat(state.itemOutcomes[instanceKey]?.[variableName] ?? null);
3841
+ }
3842
+ }
3843
+ return null;
3844
+ }
3845
+ if (variableName === "duration") {
3846
+ if (itemsByKey.has(itemKey)) {
3847
+ const seconds = state.itemDurationSeconds?.[itemKey];
3848
+ return seconds === undefined ? null : durationValue(seconds);
3849
+ }
3850
+ const partSeconds = state.timing?.partSeconds[itemKey];
3851
+ if (plan.parts.some((part) => part.identifier === itemKey)) {
3852
+ return partSeconds === undefined ? null : durationValue(partSeconds);
3853
+ }
3854
+ if (plan.sections[itemKey]) {
3855
+ const seconds = state.timing?.sectionSeconds[itemKey];
3856
+ return seconds === undefined ? null : durationValue(seconds);
3857
+ }
3858
+ return null;
3859
+ }
3860
+ return liftFlat(state.itemOutcomes[itemKey]?.[variableName] ?? null);
3861
+ }
3862
+ if (outcomes?.has(identifier)) {
3863
+ return outcomes.get(identifier) ?? null;
3864
+ }
3865
+ return liftFlat(state.testOutcomes[identifier] ?? null);
3866
+ },
3867
+ responseDeclaration: () => {
3868
+ return;
3869
+ },
3870
+ responseValue: () => null,
3871
+ testVariables: (expression) => {
3872
+ const variableName = expression.variableIdentifier ?? expression.identifier ?? "";
3873
+ const weightIdentifier = expression.weightIdentifier;
3874
+ const members = [];
3875
+ let baseType = expression.baseType;
3876
+ for (const item of subsetItems(expression)) {
3877
+ const value = state.itemOutcomes[item.key]?.[variableName];
3878
+ if (value === undefined || value === null) {
3879
+ continue;
3880
+ }
3881
+ const lifted = liftFlat(value);
3882
+ if (lifted === null) {
3883
+ continue;
3884
+ }
3885
+ if (weightIdentifier !== undefined && isNumericBaseType(lifted.baseType)) {
3886
+ baseType = "float";
3887
+ members.push(...lifted.values.map((entry) => Number(entry) * weightOf(item, weightIdentifier)));
3888
+ continue;
3889
+ }
3890
+ baseType ??= lifted.baseType;
3891
+ members.push(...lifted.values);
3892
+ }
3893
+ return members.length === 0 ? null : rpValue("multiple", members, baseType);
3894
+ },
3895
+ testAggregate: (expression) => {
3896
+ const subset = subsetItems(expression);
3897
+ const integer = (value) => ({
3898
+ cardinality: "single",
3899
+ baseType: "integer",
3900
+ values: [value]
3901
+ });
3902
+ const countIn = (list) => {
3903
+ const flagged = new Set(list ?? []);
3904
+ return subset.filter((item) => flagged.has(item.key)).length;
3905
+ };
3906
+ switch (expression.kind) {
3907
+ case "numberSelected":
3908
+ return integer(subset.length);
3909
+ case "numberPresented":
3910
+ return integer(countIn(state.presentedItems));
3911
+ case "numberResponded":
3912
+ return integer(countIn(state.respondedItems));
3913
+ case "numberCorrect":
3914
+ return integer(countIn(state.correctItems));
3915
+ case "numberIncorrect":
3916
+ return integer(countIn(state.incorrectItems));
3917
+ case "outcomeMinimum":
3918
+ case "outcomeMaximum": {
3919
+ const bound = expression.kind === "outcomeMaximum" ? "normalMaximum" : "normalMinimum";
3920
+ const members = [];
3921
+ for (const item of subset) {
3922
+ const declared = options.itemOutcomeDeclarations?.[item.ref.identifier]?.find((entry) => entry.identifier === expression.outcomeIdentifier)?.[bound];
3923
+ if (declared === undefined) {
3924
+ if (expression.kind === "outcomeMaximum") {
3925
+ return null;
3926
+ }
3927
+ continue;
3928
+ }
3929
+ members.push(declared * weightOf(item, expression.weightIdentifier));
3930
+ }
3931
+ return members.length === 0 ? null : rpValue("multiple", members, "float");
3932
+ }
3933
+ default:
3934
+ throw new RpUnsupportedError(expression.kind);
3935
+ }
3936
+ }
3937
+ };
3938
+ }
3939
+ function conditionPasses(expression, state) {
3940
+ try {
3941
+ return singleBoolean(evaluateExpression(expression, makeEnv(state))) === true;
3942
+ } catch (error) {
3943
+ if (error instanceof RpUnsupportedError) {
3944
+ return true;
3945
+ }
3946
+ throw error;
3947
+ }
3948
+ }
3949
+ function preConditionsPass(item, state) {
3950
+ return item.preConditions.every((expression) => conditionPasses(expression, state));
3951
+ }
3952
+ function runOutcomeProcessing(state) {
3953
+ let outcomes = defaultTestOutcomes();
3954
+ const env = makeEnv(state, outcomes);
3955
+ function branchTaken(branch) {
3956
+ if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
3957
+ return false;
3958
+ }
3959
+ executeRules(branch.rules);
3960
+ return true;
3961
+ }
3962
+ function executeRules(rules) {
3963
+ for (const rule of rules) {
3964
+ if (!supportedOutcomeRuleKinds.has(rule.kind)) {
3965
+ throw new RpUnsupportedError(rule.kind);
3966
+ }
3967
+ if (rule.kind === "exitTest") {
3968
+ throw new ExitTestSignal;
3969
+ }
3970
+ if (rule.kind === "setOutcomeValue") {
3971
+ if (rule.identifier !== undefined && rule.expression !== undefined) {
3972
+ outcomes.set(rule.identifier, evaluateExpression(rule.expression, env));
3973
+ }
3974
+ continue;
3975
+ }
3976
+ if (rule.kind === "outcomeProcessingFragment") {
3977
+ executeRules(rule.rules ?? []);
3978
+ continue;
3979
+ }
3980
+ if (rule.kind === "lookupOutcomeValue") {
3981
+ if (rule.identifier !== undefined && rule.expression !== undefined) {
3982
+ const declaration = (view.outcomeDeclarations ?? []).find((entry) => entry.identifier === rule.identifier);
3983
+ if (!hasLookupTable(declaration)) {
3984
+ throw new RpUnsupportedError("lookupOutcomeValue");
3985
+ }
3986
+ outcomes.set(rule.identifier, lookupTableValue(declaration, evaluateExpression(rule.expression, env)));
3987
+ }
3988
+ continue;
3989
+ }
3990
+ if (rule.outcomeIf && branchTaken(rule.outcomeIf)) {
3991
+ continue;
3992
+ }
3993
+ const elseIfTaken = (rule.outcomeElseIfs ?? []).some((branch) => branchTaken(branch));
3994
+ if (!elseIfTaken && rule.outcomeElse) {
3995
+ executeRules(rule.outcomeElse.rules);
3996
+ }
3997
+ }
3998
+ }
3999
+ try {
4000
+ executeRules(view.outcomeProcessing?.rules ?? []);
4001
+ } catch (error) {
4002
+ if (error instanceof RpUnsupportedError) {
4003
+ outcomes = defaultTestOutcomes();
4004
+ } else if (!(error instanceof ExitTestSignal)) {
4005
+ throw error;
4006
+ }
4007
+ }
4008
+ return Object.fromEntries([...outcomes].map(([identifier, value]) => [identifier, toOutcomeValue(value)]));
4009
+ }
4010
+ function firstNavigable(state, partIndex, itemIndex) {
4011
+ for (let p2 = partIndex;p2 < plan.parts.length; p2 += 1) {
4012
+ const items = plan.parts[p2].items;
4013
+ for (let i3 = p2 === partIndex ? itemIndex : 0;i3 < items.length; i3 += 1) {
4014
+ const item = items[i3];
4015
+ if (preConditionsPass(item, state) && navigableInTime(state, item)) {
4016
+ return item;
4017
+ }
4018
+ }
4019
+ }
4020
+ return null;
4021
+ }
4022
+ function positionOf(itemKey) {
4023
+ const partIndex = partIndexByItemKey.get(itemKey);
4024
+ if (partIndex === undefined) {
4025
+ return null;
4026
+ }
4027
+ const itemIndex = plan.parts[partIndex].items.findIndex((item) => item.key === itemKey);
4028
+ return itemIndex === -1 ? null : { partIndex, itemIndex };
4029
+ }
4030
+ function withFlag(list, itemKey, present) {
4031
+ const existing = list ?? [];
4032
+ if (existing.includes(itemKey) === present) {
4033
+ return existing;
4034
+ }
4035
+ return present ? [...existing, itemKey] : existing.filter((entry) => entry !== itemKey);
4036
+ }
4037
+ function applyResultFlags(state, itemKey, result) {
4038
+ return {
4039
+ ...state,
4040
+ respondedItems: withFlag(state.respondedItems, itemKey, result.responded === true),
4041
+ correctItems: withFlag(state.correctItems, itemKey, result.correct === true),
4042
+ incorrectItems: withFlag(state.incorrectItems, itemKey, result.correct === false)
4043
+ };
4044
+ }
4045
+ function markPresented(state, itemKey) {
4046
+ return (state.presentedItems ?? []).includes(itemKey) ? state : { ...state, presentedItems: [...state.presentedItems ?? [], itemKey] };
4047
+ }
4048
+ function withRecordedAttempt(state, itemKey, result, atMs) {
4049
+ const entry = {
4050
+ atMs,
4051
+ outcomes: result.outcomes,
4052
+ ...result.responses !== undefined ? { responses: result.responses } : {},
4053
+ ...result.durationSeconds !== undefined ? { durationSeconds: result.durationSeconds } : {}
4054
+ };
4055
+ return {
4056
+ ...state,
4057
+ attemptHistory: {
4058
+ ...state.attemptHistory ?? {},
4059
+ [itemKey]: [...state.attemptHistory?.[itemKey] ?? [], entry]
4060
+ }
4061
+ };
4062
+ }
4063
+ function flushPending(state, partIndex) {
4064
+ const pending = state.pendingItemResults ?? {};
4065
+ const keys = Object.keys(pending).filter((key) => partIndex === null || partIndexByItemKey.get(key) === partIndex);
4066
+ if (keys.length === 0) {
4067
+ return state;
4068
+ }
4069
+ const itemOutcomes = { ...state.itemOutcomes };
4070
+ const attemptCounts = { ...state.attemptCounts ?? {} };
4071
+ const itemDurations = { ...state.itemDurationSeconds ?? {} };
4072
+ const remaining = { ...pending };
4073
+ let flagged = state;
4074
+ for (const key of keys) {
4075
+ const result = pending[key];
4076
+ itemOutcomes[key] = result.outcomes;
4077
+ attemptCounts[key] = (attemptCounts[key] ?? 0) + 1;
4078
+ delete remaining[key];
4079
+ if (result.durationSeconds !== undefined) {
4080
+ itemDurations[key] = result.durationSeconds;
4081
+ }
4082
+ flagged = withRecordedAttempt(applyResultFlags(flagged, key, result), key, result, result.submittedAtMs ?? now());
4083
+ }
4084
+ return {
4085
+ ...flagged,
4086
+ itemOutcomes,
4087
+ attemptCounts,
4088
+ itemDurationSeconds: itemDurations,
4089
+ pendingItemResults: remaining
4090
+ };
4091
+ }
4092
+ function ended(state) {
4093
+ const flushed = flushPending(state, null);
4094
+ return { ...flushed, status: "ended", currentItemKey: null, testOutcomes: runOutcomeProcessing(flushed) };
4095
+ }
4096
+ function evaluateTemplateDefaults(state, item) {
4097
+ const defaults = item.ref.templateDefaults;
4098
+ if (!defaults || defaults.length === 0) {
4099
+ return;
4100
+ }
4101
+ const env = makeEnv(state);
4102
+ const values = {};
4103
+ for (const entry of defaults) {
4104
+ try {
4105
+ values[entry.templateIdentifier] = toOutcomeValue(evaluateExpression(entry.expression, env));
4106
+ } catch (error) {
4107
+ if (!(error instanceof RpUnsupportedError)) {
4108
+ throw error;
4109
+ }
4110
+ }
4111
+ }
4112
+ return values;
4113
+ }
4114
+ function withTemplateDefaults(state, items) {
4115
+ let merged = state.templateDefaultValues ?? {};
4116
+ let changed = false;
4117
+ for (const item of items) {
4118
+ if (merged[item.key] !== undefined) {
4119
+ continue;
4120
+ }
4121
+ const values = evaluateTemplateDefaults(state, item);
4122
+ if (values) {
4123
+ merged = { ...merged, [item.key]: values };
4124
+ changed = true;
4125
+ }
4126
+ }
4127
+ return changed ? { ...state, templateDefaultValues: merged } : state;
4128
+ }
4129
+ function moveToItem(state, item) {
4130
+ if (item === null) {
4131
+ return ended(state);
4132
+ }
4133
+ const fromPart = state.currentItemKey === null ? undefined : partIndexByItemKey.get(state.currentItemKey);
4134
+ const toPart = partIndexByItemKey.get(item.key);
4135
+ let next = state;
4136
+ if (fromPart !== undefined && toPart !== fromPart) {
4137
+ const flushed = flushPending(state, fromPart);
4138
+ if (flushed !== state) {
4139
+ next = { ...flushed, testOutcomes: runOutcomeProcessing(flushed) };
4140
+ }
4141
+ }
4142
+ const part = toPart === undefined ? undefined : plan.parts[toPart];
4143
+ if (part) {
4144
+ next = withTemplateDefaults(next, part.navigationMode === "nonlinear" ? part.items : [item]);
4145
+ }
4146
+ return markPresented({ ...next, currentItemKey: item.key }, item.key);
4147
+ }
4148
+ function nextState(state) {
4149
+ if (state.status !== "in-progress" || state.currentItemKey === null) {
4150
+ return state;
4151
+ }
4152
+ const current = positionOf(state.currentItemKey);
4153
+ if (!current) {
4154
+ return ended(state);
4155
+ }
4156
+ const part = plan.parts[current.partIndex];
4157
+ const currentItem = part.items[current.itemIndex];
4158
+ if (part.navigationMode === "linear" && !currentItem.sessionControl.allowSkipping && !state.attemptedItems.includes(currentItem.key)) {
4159
+ return state;
4160
+ }
4161
+ if (part.navigationMode === "linear" && minTimeBlocked(state, currentItem, firstNavigable(state, current.partIndex, current.itemIndex + 1))) {
4162
+ return state;
4163
+ }
4164
+ for (const branchRule of currentItem.ref.branchRules ?? []) {
4165
+ if (!conditionPasses(branchRule.expression, state)) {
4166
+ continue;
4167
+ }
4168
+ if (branchRule.target === "EXIT_TEST") {
4169
+ return ended(state);
4170
+ }
4171
+ if (branchRule.target === "EXIT_TESTPART") {
4172
+ return moveToItem(state, firstNavigable(state, current.partIndex + 1, 0));
4173
+ }
4174
+ if (branchRule.target === "EXIT_SECTION") {
4175
+ const items = part.items;
4176
+ const sectionKey = currentItem.sectionPath.join("/");
4177
+ let index = current.itemIndex + 1;
4178
+ while (index < items.length && items[index].sectionPath.join("/") === sectionKey) {
4179
+ index += 1;
4180
+ }
4181
+ return moveToItem(state, firstNavigable(state, current.partIndex, index));
4182
+ }
4183
+ const target = positionOf(branchRule.target) ?? (instancesByRef.get(branchRule.target) ?? []).map((instance) => positionOf(instance.key)).find((position) => position !== null && position.partIndex === current.partIndex && position.itemIndex > current.itemIndex) ?? null;
4184
+ if (target && target.partIndex === current.partIndex) {
4185
+ return moveToItem(state, firstNavigable(state, target.partIndex, target.itemIndex));
4186
+ }
4187
+ }
4188
+ const destination = firstNavigable(state, current.partIndex, current.itemIndex + 1);
4189
+ if (part.navigationMode === "nonlinear" && (destination === null || partIndexByItemKey.get(destination.key) !== current.partIndex)) {
4190
+ const blocked = part.items.some((item) => !item.sessionControl.allowSkipping && !state.attemptedItems.includes(item.key) && preConditionsPass(item, state) && navigableInTime(state, item));
4191
+ if (blocked) {
4192
+ return state;
4193
+ }
4194
+ }
4195
+ return moveToItem(state, destination);
4196
+ }
4197
+ function reviewBarred(state, item) {
4198
+ return item.sessionControl.allowReview === false && remainingAttempts(state, item.key) <= 0;
4199
+ }
4200
+ function reviewable(state, itemKey) {
4201
+ const item = itemsByKey.get(itemKey);
4202
+ return state.status === "ended" && item !== undefined && item.sessionControl.allowReview && (state.presentedItems ?? []).includes(itemKey);
4203
+ }
4204
+ function commentable(state, itemKey) {
4205
+ return state.status === "in-progress" && itemsByKey.get(itemKey)?.sessionControl.allowComment === true;
4206
+ }
4207
+ function submitBody(state, itemKey, result) {
4208
+ const partIndex = partIndexByItemKey.get(itemKey);
4209
+ if (partIndex !== undefined && plan.parts[partIndex].submissionMode === "simultaneous") {
4210
+ if (attemptsOf(state, itemKey) > 0) {
4211
+ return state;
4212
+ }
4213
+ return {
4214
+ ...state,
4215
+ pendingItemResults: { ...state.pendingItemResults ?? {}, [itemKey]: { ...result, submittedAtMs: now() } },
4216
+ attemptedItems: state.attemptedItems.includes(itemKey) ? state.attemptedItems : [...state.attemptedItems, itemKey]
4217
+ };
4218
+ }
4219
+ if (result.valid === false && itemsByKey.get(itemKey)?.sessionControl.validateResponses === true) {
4220
+ return state;
4221
+ }
4222
+ if (result.adaptive !== true && remainingAttempts(state, itemKey) <= 0) {
4223
+ return state;
4224
+ }
4225
+ const next = {
4226
+ ...withRecordedAttempt(applyResultFlags(state, itemKey, result), itemKey, result, now()),
4227
+ itemOutcomes: { ...state.itemOutcomes, [itemKey]: result.outcomes },
4228
+ attemptedItems: state.attemptedItems.includes(itemKey) ? state.attemptedItems : [...state.attemptedItems, itemKey],
4229
+ attemptCounts: { ...state.attemptCounts ?? {}, [itemKey]: attemptsOf(state, itemKey) + 1 },
4230
+ ...result.durationSeconds !== undefined ? { itemDurationSeconds: { ...state.itemDurationSeconds ?? {}, [itemKey]: result.durationSeconds } } : {}
4231
+ };
4232
+ return { ...next, testOutcomes: runOutcomeProcessing(next) };
4233
+ }
4234
+ function applyExpiries(state) {
4235
+ if (state.status !== "in-progress") {
4236
+ return state;
4237
+ }
4238
+ if (scopeExpired(state, { kind: "test" })) {
4239
+ return ended(state);
4240
+ }
4241
+ const currentKey = state.currentItemKey;
4242
+ const item = currentKey === null ? undefined : itemsByKey.get(currentKey);
4243
+ if (!item || navigableInTime(state, item)) {
4244
+ return state;
4245
+ }
4246
+ const position = positionOf(item.key);
4247
+ return position === null ? ended(state) : moveToItem(state, firstNavigable(state, position.partIndex, position.itemIndex + 1));
4248
+ }
4249
+ function withTransition(state, op) {
4250
+ if (state.status !== "in-progress") {
4251
+ return state;
4252
+ }
4253
+ const touched = touch(state);
4254
+ const settled = applyExpiries(touched);
4255
+ if (settled !== touched) {
4256
+ return settled;
4257
+ }
4258
+ const result = op(settled);
4259
+ return result === settled ? state : result;
4260
+ }
4261
+ const issues = [];
4262
+ const seenIssues = new Set;
4263
+ function report(name) {
4264
+ if (!seenIssues.has(name)) {
4265
+ seenIssues.add(name);
4266
+ issues.push({ type: "unsupported-rp", name });
4267
+ }
4268
+ }
4269
+ function walkOutcomeRules(rules) {
4270
+ for (const rule of rules) {
4271
+ if (!supportedOutcomeRuleKinds.has(rule.kind)) {
4272
+ report(rule.kind);
4273
+ continue;
4274
+ }
4275
+ if (rule.kind === "lookupOutcomeValue") {
4276
+ const declaration = (view.outcomeDeclarations ?? []).find((entry) => entry.identifier === rule.identifier);
4277
+ if (!hasLookupTable(declaration)) {
4278
+ report("lookupOutcomeValue");
4279
+ }
4280
+ }
4281
+ if (rule.expression) {
4282
+ collectExpressionIssues(rule.expression, testExpressionKinds, report);
4283
+ }
4284
+ if (rule.rules) {
4285
+ walkOutcomeRules(rule.rules);
4286
+ }
4287
+ for (const branch of [rule.outcomeIf, ...rule.outcomeElseIfs ?? []]) {
4288
+ if (branch) {
4289
+ collectExpressionIssues(branch.expression, testExpressionKinds, report);
4290
+ walkOutcomeRules(branch.rules);
4291
+ }
4292
+ }
4293
+ if (rule.outcomeElse) {
4294
+ walkOutcomeRules(rule.outcomeElse.rules);
4295
+ }
4296
+ }
4297
+ }
4298
+ walkOutcomeRules(view.outcomeProcessing?.rules ?? []);
4299
+ for (const item of allItems) {
4300
+ for (const expression of item.preConditions) {
4301
+ collectExpressionIssues(expression, testExpressionKinds, report);
4302
+ }
4303
+ for (const branchRule of item.ref.branchRules ?? []) {
4304
+ collectExpressionIssues(branchRule.expression, testExpressionKinds, report);
4305
+ }
4306
+ for (const entry of item.ref.templateDefaults ?? []) {
4307
+ collectExpressionIssues(entry.expression, testExpressionKinds, report);
4308
+ }
4309
+ }
4310
+ return {
4311
+ test: view,
4312
+ plan,
4313
+ issues,
4314
+ start: () => {
4315
+ const initial = {
4316
+ status: "in-progress",
4317
+ currentItemKey: null,
4318
+ itemOutcomes: {},
4319
+ attemptedItems: [],
4320
+ attemptCounts: {},
4321
+ presentedItems: [],
4322
+ respondedItems: [],
4323
+ correctItems: [],
4324
+ incorrectItems: [],
4325
+ pendingItemResults: {},
4326
+ testOutcomes: {},
4327
+ timing: {
4328
+ lastTransitionAtMs: now(),
4329
+ testSeconds: 0,
4330
+ partSeconds: {},
4331
+ sectionSeconds: {},
4332
+ itemSeconds: {}
4333
+ }
4334
+ };
4335
+ const opened = { ...initial, testOutcomes: runOutcomeProcessing(initial) };
4336
+ return moveToItem(opened, firstNavigable(opened, 0, 0));
4337
+ },
4338
+ currentItem: (state) => state.currentItemKey === null ? null : allItems.find((item) => item.key === state.currentItemKey) ?? null,
4339
+ canMoveTo: (state, itemKey) => {
4340
+ if (state.status !== "in-progress" || state.currentItemKey === null) {
4341
+ return false;
4342
+ }
4343
+ const current = positionOf(state.currentItemKey);
4344
+ const target = positionOf(itemKey);
4345
+ if (!current || !target || target.partIndex !== current.partIndex) {
4346
+ return false;
4347
+ }
4348
+ if (plan.parts[current.partIndex].navigationMode !== "nonlinear") {
4349
+ return false;
4350
+ }
4351
+ const item = plan.parts[target.partIndex].items[target.itemIndex];
4352
+ return preConditionsPass(item, state) && navigableInTime(state, item) && !reviewBarred(state, item);
4353
+ },
4354
+ moveTo: (state, itemKey) => withTransition(state, (settled) => {
4355
+ const current = positionOf(settled.currentItemKey ?? "");
4356
+ const target = positionOf(itemKey);
4357
+ const item = itemsByKey.get(itemKey);
4358
+ if (!current || !target || !item || target.partIndex !== current.partIndex || plan.parts[current.partIndex].navigationMode !== "nonlinear" || !navigableInTime(settled, item) || reviewBarred(settled, item)) {
4359
+ return settled;
4360
+ }
4361
+ return markPresented({ ...settled, currentItemKey: itemKey }, itemKey);
4362
+ }),
4363
+ canNext: (state) => nextState(state) !== state,
4364
+ next: (state) => withTransition(state, nextState),
4365
+ remainingAttempts,
4366
+ canSubmitItem: (state, itemKey) => {
4367
+ if (state.status !== "in-progress" || remainingAttempts(state, itemKey) <= 0) {
4368
+ return false;
4369
+ }
4370
+ const item = itemsByKey.get(itemKey);
4371
+ return item !== undefined && enclosingScopes(item).every((scope) => !scopeExpired(state, scope) || timeLimitsOf(scope)?.allowLateSubmission === true);
4372
+ },
4373
+ submitItem: (state, itemKey, result) => {
4374
+ const item = itemsByKey.get(itemKey);
4375
+ if (state.status !== "in-progress" || !item) {
4376
+ return state;
4377
+ }
4378
+ const touched = touch(state);
4379
+ const barring = enclosingScopes(item).find((scope) => scopeExpired(touched, scope) && timeLimitsOf(scope)?.allowLateSubmission !== true);
4380
+ if (barring) {
4381
+ return applyExpiries({
4382
+ ...touched,
4383
+ rejectedSubmissions: [
4384
+ ...touched.rejectedSubmissions ?? [],
4385
+ { itemKey, scope: barring, atTestSeconds: touched.timing?.testSeconds ?? 0 }
4386
+ ]
4387
+ });
4388
+ }
4389
+ const accepted = submitBody(touched, itemKey, result);
4390
+ return accepted === touched ? state : applyExpiries(accepted);
4391
+ },
4392
+ end: (state) => state.status === "ended" ? state : ended(touch(state)),
4393
+ tick: (state) => state.status !== "in-progress" ? state : applyExpiries(touch(state)),
4394
+ suspend: (state) => {
4395
+ if (state.status !== "in-progress") {
4396
+ return state;
4397
+ }
4398
+ const settled = applyExpiries(touch(state));
4399
+ return settled.status === "in-progress" ? { ...settled, status: "suspended" } : settled;
4400
+ },
4401
+ resume: (state) => state.status !== "suspended" ? state : {
4402
+ ...state,
4403
+ status: "in-progress",
4404
+ ...state.timing ? { timing: { ...state.timing, lastTransitionAtMs: now() } } : {}
4405
+ },
4406
+ canReview: reviewable,
4407
+ review: (state, itemKey) => reviewable(state, itemKey) ? { ...state, currentItemKey: itemKey } : state,
4408
+ canComment: commentable,
4409
+ setItemComment: (state, itemKey, comment) => commentable(state, itemKey) ? { ...state, itemComments: { ...state.itemComments ?? {}, [itemKey]: comment } } : state,
4410
+ visibleTestFeedbacks: (state) => (view.testFeedbacks ?? []).filter((feedback) => {
4411
+ const accessOk = (feedback.access ?? "atEnd") === (state.status === "ended" ? "atEnd" : "during");
4412
+ if (!accessOk) {
4413
+ return false;
4414
+ }
4415
+ const outcome = state.testOutcomes[feedback.outcomeIdentifier] ?? null;
4416
+ const matched = Array.isArray(outcome) ? outcome.map(String).includes(feedback.identifier) : outcome !== null && String(outcome) === feedback.identifier;
4417
+ return matched !== (feedback.showHide === "hide");
4418
+ })
4419
+ };
4420
+ }
4421
+ // src/test/results.ts
4422
+ function iso(ms) {
4423
+ return new Date(ms).toISOString();
4424
+ }
4425
+ function pnpSupports(pnp) {
4426
+ if (!pnp) {
4427
+ return [];
4428
+ }
4429
+ const activation = resolvePnpActivation(pnp);
4430
+ const names = [...new Set([...activation.active, ...activation.optional, ...activation.prohibited])].sort();
4431
+ return names.map((name) => {
4432
+ if (activation.prohibited.has(name)) {
4433
+ return { name, assignment: "prohibited" };
4434
+ }
4435
+ const preference = pnpFeaturePreference(pnp, name);
4436
+ const xmlLang = typeof preference?.["xmlLang"] === "string" ? preference["xmlLang"] : undefined;
4437
+ let value;
4438
+ if (name === "additional-testing-time") {
4439
+ const time = pnp.additionalTestingTime;
4440
+ value = time?.unlimited === true ? "unlimited" : time?.timeMultiplier !== undefined ? String(time.timeMultiplier) : time?.fixedMinutes !== undefined ? String(time.fixedMinutes) : undefined;
4441
+ }
4442
+ return {
4443
+ name,
4444
+ assignment: "assigned",
4445
+ ...value !== undefined ? { value } : {},
4446
+ ...xmlLang !== undefined ? { xmlLang } : {}
4447
+ };
4448
+ });
4449
+ }
4450
+ function valueViews(value) {
4451
+ if (value === null || value === undefined || value === "") {
4452
+ return [];
4453
+ }
4454
+ if (Array.isArray(value)) {
4455
+ return value.filter((member) => member !== null && member !== "").map((member) => ({ value: String(member) }));
4456
+ }
4457
+ if (typeof value === "object") {
4458
+ return Object.entries(value).filter(([, member]) => member !== null && member !== "").map(([fieldIdentifier, member]) => ({ fieldIdentifier, value: String(member) }));
4459
+ }
4460
+ return [{ value: String(value) }];
4461
+ }
4462
+ function inferBaseType2(value, identifier) {
4463
+ const sample = Array.isArray(value) ? value[0] : value;
4464
+ if (typeof sample === "number") {
4465
+ return "float";
4466
+ }
4467
+ if (typeof sample === "boolean") {
4468
+ return "boolean";
4469
+ }
4470
+ if (identifier === "completionStatus" || identifier === "completion_status") {
4471
+ return "identifier";
4472
+ }
4473
+ return typeof sample === "string" ? "string" : undefined;
4474
+ }
4475
+ function outcomeVariablesOf(outcomes, declarations) {
4476
+ return Object.entries(outcomes).map(([identifier, value]) => {
4477
+ const declaration = declarations?.find((entry) => entry.identifier === identifier);
4478
+ const cardinality = declaration?.cardinality ?? (Array.isArray(value) ? "multiple" : "single");
4479
+ const baseType = declaration?.baseType ?? inferBaseType2(value, identifier);
4480
+ return {
4481
+ identifier,
4482
+ cardinality,
4483
+ ...baseType !== undefined && cardinality !== "record" ? { baseType } : {},
4484
+ values: valueViews(value)
4485
+ };
4486
+ });
4487
+ }
4488
+ function durationVariable(identifier, seconds) {
4489
+ return {
4490
+ identifier,
4491
+ cardinality: "single",
4492
+ baseType: "duration",
4493
+ candidateResponse: { values: [{ value: String(seconds) }] }
4494
+ };
4495
+ }
4496
+ function numAttemptsVariable(count) {
4497
+ return {
4498
+ identifier: "numAttempts",
4499
+ cardinality: "single",
4500
+ baseType: "integer",
4501
+ candidateResponse: { values: [{ value: String(count) }] }
4502
+ };
4503
+ }
4504
+ function responseVariablesOf(responses, details) {
4505
+ return Object.entries(responses ?? {}).map(([identifier, value]) => {
4506
+ const declaration = details?.responseDeclarations?.find((entry) => entry.identifier === identifier);
4507
+ const cardinality = declaration?.cardinality ?? (Array.isArray(value) ? "multiple" : "single");
4508
+ const baseType = declaration?.baseType;
4509
+ const correct = details?.correctResponses?.[identifier];
4510
+ const correctValues = correct === undefined ? [] : valueViews(correct);
4511
+ return {
4512
+ identifier,
4513
+ cardinality,
4514
+ ...baseType !== undefined && cardinality !== "record" ? { baseType } : {},
4515
+ candidateResponse: { values: valueViews(value) },
4516
+ ...correctValues.length > 0 ? { correctResponse: { values: correctValues } } : {}
4517
+ };
4518
+ });
4519
+ }
4520
+ function buildAssessmentResult(input) {
4521
+ const { test, plan, state } = input;
4522
+ const nowMs = input.nowMs ?? Date.now();
4523
+ const timing = state.timing;
4524
+ const scopeDurations = timing ? [
4525
+ durationVariable("duration", timing.testSeconds),
4526
+ ...plan.parts.filter((part) => timing.partSeconds[part.identifier] !== undefined).map((part) => durationVariable(`${part.identifier}.duration`, timing.partSeconds[part.identifier])),
4527
+ ...Object.keys(plan.sections).filter((identifier) => timing.sectionSeconds[identifier] !== undefined).map((identifier) => durationVariable(`${identifier}.duration`, timing.sectionSeconds[identifier]))
4528
+ ] : [];
4529
+ const testOutcomes = outcomeVariablesOf(state.testOutcomes, test.outcomeDeclarations);
4530
+ const supports = pnpSupports(input.pnp);
4531
+ const testResult = {
4532
+ identifier: test.identifier,
4533
+ datestamp: iso(nowMs),
4534
+ ...scopeDurations.length > 0 ? { responseVariables: scopeDurations } : {},
4535
+ ...testOutcomes.length > 0 ? { outcomeVariables: testOutcomes } : {},
4536
+ ...supports.length > 0 ? { supports } : {}
4537
+ };
4538
+ const itemResults = [];
4539
+ let sequenceIndex = 0;
4540
+ for (const part of plan.parts) {
4541
+ for (const item of part.items) {
4542
+ sequenceIndex += 1;
4543
+ const details = input.itemDetails?.(item) ?? null;
4544
+ const entries = [];
4545
+ (state.attemptHistory?.[item.key] ?? []).forEach((attempt, index) => {
4546
+ const outcomeVariables = outcomeVariablesOf(attempt.outcomes, details?.outcomeDeclarations);
4547
+ entries.push({
4548
+ identifier: item.key,
4549
+ sequenceIndex,
4550
+ datestamp: iso(attempt.atMs),
4551
+ sessionStatus: "final",
4552
+ responseVariables: [
4553
+ numAttemptsVariable(index + 1),
4554
+ ...attempt.durationSeconds !== undefined ? [durationVariable("duration", attempt.durationSeconds)] : [],
4555
+ ...responseVariablesOf(attempt.responses, details)
4556
+ ],
4557
+ ...outcomeVariables.length > 0 ? { outcomeVariables } : {}
4558
+ });
4559
+ });
4560
+ const pending = state.pendingItemResults?.[item.key];
4561
+ if (pending) {
4562
+ entries.push({
4563
+ identifier: item.key,
4564
+ sequenceIndex,
4565
+ datestamp: iso(pending.submittedAtMs ?? nowMs),
4566
+ sessionStatus: "pendingResponseProcessing",
4567
+ responseVariables: [
4568
+ numAttemptsVariable(1),
4569
+ ...pending.durationSeconds !== undefined ? [durationVariable("duration", pending.durationSeconds)] : [],
4570
+ ...responseVariablesOf(pending.responses, details)
4571
+ ]
4572
+ });
4573
+ }
4574
+ if (entries.length === 0) {
4575
+ const itemSeconds = timing?.itemSeconds[item.key];
4576
+ entries.push({
4577
+ identifier: item.key,
4578
+ sequenceIndex,
4579
+ datestamp: iso(nowMs),
4580
+ sessionStatus: "initial",
4581
+ responseVariables: [
4582
+ numAttemptsVariable(0),
4583
+ ...itemSeconds !== undefined ? [durationVariable("duration", itemSeconds)] : []
4584
+ ]
4585
+ });
4586
+ }
4587
+ const comment = state.itemComments?.[item.key];
4588
+ if (comment !== undefined) {
4589
+ entries[entries.length - 1] = { ...entries[entries.length - 1], candidateComment: comment };
4590
+ }
4591
+ itemResults.push(...entries);
4592
+ }
4593
+ }
4594
+ return {
4595
+ assessmentResult: {
4596
+ context: input.context ?? {},
4597
+ testResult,
4598
+ ...itemResults.length > 0 ? { itemResults } : {}
4599
+ }
4600
+ };
4601
+ }
4602
+ // src/test/session-store.ts
4603
+ function collectCorrectResponseTargets(rules, into) {
4604
+ for (const rule of rules ?? []) {
4605
+ if (rule.kind === "setCorrectResponse" && rule.identifier !== undefined) {
4606
+ into.add(rule.identifier);
4607
+ }
4608
+ for (const branch of [rule.templateIf, ...rule.templateElseIfs ?? []]) {
4609
+ if (branch) {
4610
+ collectCorrectResponseTargets(branch.rules, into);
4611
+ }
4612
+ }
4613
+ if (rule.templateElse) {
4614
+ collectCorrectResponseTargets(rule.templateElse.rules, into);
4615
+ }
4616
+ }
4617
+ }
4618
+ function scorableIdentifiers(view) {
4619
+ const templated = new Set;
4620
+ collectCorrectResponseTargets(view.templateProcessing?.rules, templated);
4621
+ return new Set(view.responseDeclarations.filter((declaration) => declaration.correctResponse !== undefined || declaration.mapping !== undefined || declaration.areaMapping !== undefined || templated.has(declaration.identifier)).map((declaration) => declaration.identifier));
4622
+ }
4623
+ function hasResponse(value) {
4624
+ if (value === null || value === undefined || value === "") {
4625
+ return false;
4626
+ }
4627
+ if (isResponseRecord(value)) {
4628
+ return Object.values(value).some((member) => member !== null && member !== "");
4629
+ }
4630
+ return !Array.isArray(value) || value.length > 0;
4631
+ }
4632
+ function resultFlags(attempt, scorable) {
4633
+ const relevant = attempt.scores.filter((score) => scorable.has(score.identifier));
4634
+ return {
4635
+ ...relevant.length > 0 ? { correct: relevant.every((score) => score.correct) } : {},
4636
+ responded: Object.values(attempt.responses).some(hasResponse)
4637
+ };
4638
+ }
4639
+ function deriveItemSeed(seed, itemKey) {
4640
+ let hash = (2166136261 ^ seed) >>> 0;
4641
+ for (let index = 0;index < itemKey.length; index += 1) {
4642
+ hash = Math.imul(hash ^ itemKey.charCodeAt(index), 16777619) >>> 0;
4643
+ }
4644
+ return hash;
4645
+ }
4646
+ function createTestSessionStore(controller, options) {
4647
+ const listeners = new Set;
4648
+ const planItemsByKey = new Map;
4649
+ for (const part of controller.plan.parts) {
4650
+ for (const item of part.items) {
4651
+ planItemsByKey.set(item.key, item);
4652
+ }
4653
+ }
4654
+ const itemViews = new Map;
4655
+ const itemStores = new Map;
4656
+ const forwardedAttempts = new Map;
4657
+ let state = options.initialState ?? controller.start();
4658
+ let snapshot = buildSnapshot();
4659
+ function buildSnapshot() {
4660
+ const currentItem = controller.currentItem(state);
4661
+ return {
4662
+ state,
4663
+ currentItem,
4664
+ currentItemView: currentItem === null ? null : itemView(currentItem.key),
4665
+ visibleFeedbacks: controller.visibleTestFeedbacks(state)
4666
+ };
4667
+ }
4668
+ function activeKeyOf(sessionState) {
4669
+ return sessionState.status === "in-progress" ? sessionState.currentItemKey : null;
4670
+ }
4671
+ function emit(next) {
4672
+ const previousActive = activeKeyOf(state);
4673
+ state = next;
4674
+ snapshot = buildSnapshot();
4675
+ const nextActive = activeKeyOf(next);
4676
+ if (previousActive !== nextActive) {
4677
+ if (previousActive !== null) {
4678
+ itemStores.get(previousActive)?.suspend();
4679
+ }
4680
+ if (nextActive !== null) {
4681
+ itemStores.get(nextActive)?.resume();
4682
+ }
4683
+ }
4684
+ for (const listener of listeners) {
4685
+ listener();
4686
+ }
4687
+ }
4688
+ function itemView(itemKey) {
4689
+ if (!itemViews.has(itemKey)) {
4690
+ const planItem = planItemsByKey.get(itemKey);
4691
+ itemViews.set(itemKey, planItem ? options.resolveItem(planItem.ref) : null);
4692
+ }
4693
+ return itemViews.get(itemKey) ?? null;
4694
+ }
4695
+ function itemStore(itemKey) {
4696
+ if (itemStores.has(itemKey)) {
4697
+ return itemStores.get(itemKey) ?? null;
4698
+ }
4699
+ const view = itemView(itemKey);
4700
+ const planItem = planItemsByKey.get(itemKey);
4701
+ if (!view || !planItem) {
4702
+ itemStores.set(itemKey, null);
4703
+ return null;
4704
+ }
4705
+ const individual = controller.plan.parts.some((part) => part.identifier === planItem.partIdentifier && part.submissionMode === "individual");
4706
+ const store = createAttemptStore(view.responseDeclarations, {}, {
4707
+ constraints: collectInteractionConstraints(view.itemBody.content),
4708
+ validateResponses: planItem.sessionControl.validateResponses && individual,
4709
+ outcomeDeclarations: view.outcomeDeclarations,
4710
+ responseProcessing: view.responseProcessing,
4711
+ templateDeclarations: view.templateDeclarations,
4712
+ templateProcessing: view.templateProcessing,
4713
+ adaptive: view.adaptive,
4714
+ seed: deriveItemSeed(options.seed, itemKey),
4715
+ normalization: options.normalization,
4716
+ customOperators: options.customOperators,
4717
+ templateDefaultValues: state.templateDefaultValues?.[itemKey],
4718
+ now: options.now
4719
+ });
4720
+ if (activeKeyOf(state) !== itemKey) {
4721
+ store.suspend();
4722
+ }
4723
+ const scorable = scorableIdentifiers(view);
4724
+ store.subscribe(() => {
4725
+ const attempt = store.getSnapshot();
4726
+ if (attempt.submitted && forwardedAttempts.get(itemKey) !== attempt) {
4727
+ forwardedAttempts.set(itemKey, attempt);
4728
+ const next = controller.submitItem(state, itemKey, {
4729
+ outcomes: attempt.outcomes,
4730
+ ...resultFlags(attempt, scorable),
4731
+ ...view.adaptive === true ? { adaptive: true } : {},
4732
+ ...attempt.durationSeconds !== null ? { durationSeconds: attempt.durationSeconds } : {},
4733
+ valid: attempt.responseViolations.length === 0,
4734
+ responses: attempt.responses
4735
+ });
4736
+ if (next !== state) {
4737
+ emit(next);
4738
+ }
4739
+ }
4740
+ });
4741
+ itemStores.set(itemKey, store);
4742
+ return store;
4743
+ }
4744
+ return {
4745
+ controller,
4746
+ subscribe: (listener) => {
4747
+ listeners.add(listener);
4748
+ return () => listeners.delete(listener);
4749
+ },
4750
+ getSnapshot: () => snapshot,
4751
+ itemStore,
4752
+ itemView,
4753
+ next: () => emit(controller.next(state)),
4754
+ canMoveTo: (itemKey) => controller.canMoveTo(state, itemKey),
4755
+ moveTo: (itemKey) => emit(controller.moveTo(state, itemKey)),
4756
+ end: () => emit(controller.end(state)),
4757
+ tick: () => emit(controller.tick(state)),
4758
+ review: (itemKey) => emit(controller.review(state, itemKey)),
4759
+ setItemComment: (itemKey, comment) => emit(controller.setItemComment(state, itemKey, comment)),
4760
+ suspend: () => emit(controller.suspend(state)),
4761
+ resume: () => emit(controller.resume(state)),
4762
+ assessmentResult: (resultOptions) => buildAssessmentResult({
4763
+ test: controller.test,
4764
+ plan: controller.plan,
4765
+ state,
4766
+ ...resultOptions?.context !== undefined ? { context: resultOptions.context } : {},
4767
+ nowMs: resultOptions?.nowMs ?? (options.now ?? Date.now)(),
4768
+ ...options.pnp !== undefined ? { pnp: options.pnp } : {},
4769
+ itemDetails: (item) => {
4770
+ const view = itemView(item.key);
4771
+ if (!view) {
4772
+ return null;
4773
+ }
4774
+ return {
4775
+ responseDeclarations: view.responseDeclarations,
4776
+ ...view.outcomeDeclarations !== undefined ? { outcomeDeclarations: view.outcomeDeclarations } : {},
4777
+ correctResponses: itemStore(item.key)?.getSnapshot().correctResponses ?? {}
4778
+ };
4779
+ }
4780
+ })
4781
+ };
4782
+ }
4783
+ // src/rp/schema.ts
4784
+ import { z as z2 } from "zod";
4785
+ var rpScalarSchema = z2.union([z2.string(), z2.number(), z2.boolean()]);
4786
+ var numberOrRef = z2.union([z2.number(), z2.string()]);
4787
+ var categoryFilter = z2.union([z2.string(), z2.array(z2.string())]);
4788
+ var cardinalitySchema = z2.enum(["single", "multiple", "ordered", "record"]);
4789
+ var rpExpressionSchema = z2.lazy(() => z2.object({
4790
+ kind: z2.string(),
4791
+ identifier: z2.string().optional(),
4792
+ baseType: z2.string().optional(),
4793
+ value: rpScalarSchema.optional(),
4794
+ expressions: z2.array(rpExpressionSchema).optional(),
4795
+ min: numberOrRef.optional(),
4796
+ max: numberOrRef.optional(),
4797
+ step: numberOrRef.optional(),
4798
+ toleranceMode: z2.enum(["exact", "absolute", "relative"]).optional(),
4799
+ tolerance: z2.array(numberOrRef).optional(),
4800
+ includeLowerBound: z2.boolean().optional(),
4801
+ includeUpperBound: z2.boolean().optional(),
4802
+ n: numberOrRef.optional(),
4803
+ name: z2.string().optional(),
4804
+ roundingMode: z2.enum(["decimalPlaces", "significantFigures"]).optional(),
4805
+ figures: numberOrRef.optional(),
4806
+ numberRepeats: numberOrRef.optional(),
4807
+ pattern: z2.string().optional(),
4808
+ caseSensitive: z2.boolean().optional(),
4809
+ substring: z2.boolean().optional(),
4810
+ shape: z2.string().optional(),
4811
+ coords: z2.string().optional(),
4812
+ variableIdentifier: z2.string().optional(),
4813
+ outcomeIdentifier: z2.string().optional(),
4814
+ weightIdentifier: z2.string().optional(),
4815
+ sectionIdentifier: z2.string().optional(),
4816
+ includeCategory: categoryFilter.optional(),
4817
+ excludeCategory: categoryFilter.optional(),
4818
+ class: z2.string().optional(),
4819
+ definition: z2.string().optional(),
4820
+ fieldIdentifier: z2.string().optional()
4821
+ }));
4822
+ var matchTableSchema = z2.object({
4823
+ defaultValue: rpScalarSchema.optional(),
4824
+ matchTableEntries: z2.array(z2.object({ sourceValue: z2.number(), targetValue: rpScalarSchema }))
4825
+ });
4826
+ var interpolationTableSchema = z2.object({
4827
+ defaultValue: rpScalarSchema.optional(),
4828
+ interpolationTableEntries: z2.array(z2.object({
4829
+ sourceValue: z2.number(),
4830
+ targetValue: rpScalarSchema,
4831
+ includeBoundary: z2.boolean().optional()
4832
+ }))
4833
+ });
4834
+ var outcomeDeclarationSchema = z2.object({
4835
+ identifier: z2.string(),
4836
+ cardinality: cardinalitySchema,
4837
+ baseType: z2.string().optional(),
4838
+ defaultValue: z2.object({ values: z2.array(z2.object({ value: rpScalarSchema })) }).optional(),
4839
+ matchTable: matchTableSchema.optional(),
4840
+ interpolationTable: interpolationTableSchema.optional(),
4841
+ normalMaximum: z2.number().optional(),
4842
+ normalMinimum: z2.number().optional()
4843
+ });
4844
+ // src/test/schema.ts
4845
+ import { z as z3 } from "zod";
4846
+ var timeLimitsSchema = z3.object({
4847
+ minTime: z3.number().nonnegative().optional(),
4848
+ maxTime: z3.number().nonnegative().optional(),
4849
+ allowLateSubmission: z3.boolean().optional()
4850
+ });
4851
+ var itemSessionControlSchema = z3.object({
4852
+ maxAttempts: z3.number().int().nonnegative().optional(),
4853
+ showFeedback: z3.boolean().optional(),
4854
+ allowReview: z3.boolean().optional(),
4855
+ showSolution: z3.boolean().optional(),
4856
+ allowComment: z3.boolean().optional(),
4857
+ allowSkipping: z3.boolean().optional(),
4858
+ validateResponses: z3.boolean().optional()
4859
+ });
4860
+ var selectionSchema = z3.object({
4861
+ select: z3.number().int().nonnegative(),
4862
+ withReplacement: z3.boolean().optional()
4863
+ });
4864
+ var orderingSchema = z3.object({ shuffle: z3.boolean().optional() });
4865
+ var branchRuleSchema = z3.object({
4866
+ target: z3.string().trim().min(1),
4867
+ expression: rpExpressionSchema
4868
+ });
4869
+ var weightSchema = z3.object({ identifier: z3.string().trim().min(1), value: z3.number() });
4870
+ var templateDefaultSchema = z3.object({
4871
+ templateIdentifier: z3.string().trim().min(1),
4872
+ expression: rpExpressionSchema
4873
+ });
4874
+ var outcomeConditionBranchSchema = z3.lazy(() => z3.object({
4875
+ expression: rpExpressionSchema,
4876
+ rules: z3.array(outcomeRuleSchema)
4877
+ }));
4878
+ var outcomeRuleSchema = z3.lazy(() => z3.object({
4879
+ kind: z3.string(),
4880
+ identifier: z3.string().optional(),
4881
+ expression: rpExpressionSchema.optional(),
4882
+ rules: z3.array(outcomeRuleSchema).optional(),
4883
+ outcomeIf: outcomeConditionBranchSchema.optional(),
4884
+ outcomeElseIfs: z3.array(outcomeConditionBranchSchema).optional(),
4885
+ outcomeElse: z3.object({ rules: z3.array(outcomeRuleSchema) }).optional()
4886
+ }));
4887
+ var outcomeProcessingSchema = z3.object({ rules: z3.array(outcomeRuleSchema) });
4888
+ var testFeedbackSchema = z3.object({
4889
+ access: z3.enum(["atEnd", "during"]).optional(),
4890
+ outcomeIdentifier: z3.string().trim().min(1),
4891
+ identifier: z3.string().trim().min(1),
4892
+ showHide: z3.enum(["show", "hide"]).optional(),
4893
+ content: z3.array(z3.unknown()).optional()
4894
+ });
4895
+ var assessmentItemRefViewSchema = z3.object({
4896
+ kind: z3.literal("assessmentItemRef"),
4897
+ identifier: z3.string().trim().min(1),
4898
+ href: z3.string().optional(),
4899
+ categories: z3.array(z3.string()).optional(),
4900
+ fixed: z3.boolean().optional(),
4901
+ required: z3.boolean().optional(),
4902
+ preConditions: z3.array(rpExpressionSchema).optional(),
4903
+ branchRules: z3.array(branchRuleSchema).optional(),
4904
+ itemSessionControl: itemSessionControlSchema.optional(),
4905
+ timeLimits: timeLimitsSchema.optional(),
4906
+ weights: z3.array(weightSchema).optional(),
4907
+ templateDefaults: z3.array(templateDefaultSchema).optional()
4908
+ });
4909
+ var assessmentSectionFlags = {
4910
+ title: z3.string().trim().min(1).optional(),
4911
+ visible: z3.boolean().optional(),
4912
+ fixed: z3.boolean().optional(),
4913
+ required: z3.boolean().optional(),
4914
+ keepTogether: z3.boolean().optional(),
4915
+ selection: selectionSchema.optional(),
4916
+ ordering: orderingSchema.optional(),
4917
+ preConditions: z3.array(rpExpressionSchema).optional(),
4918
+ branchRules: z3.array(branchRuleSchema).optional(),
4919
+ itemSessionControl: itemSessionControlSchema.optional(),
4920
+ timeLimits: timeLimitsSchema.optional()
4921
+ };
4922
+ var testPartLevel = {
4923
+ preConditions: z3.array(rpExpressionSchema).optional(),
4924
+ branchRules: z3.array(branchRuleSchema).optional(),
4925
+ itemSessionControl: itemSessionControlSchema.optional(),
4926
+ timeLimits: timeLimitsSchema.optional()
4927
+ };
4928
+ function makeAssessmentTestSchema(itemRefSchema) {
4929
+ const assessmentSectionSchema = z3.lazy(() => z3.object({
4930
+ kind: z3.literal("assessmentSection"),
4931
+ identifier: z3.string().trim().min(1),
4932
+ ...assessmentSectionFlags,
4933
+ children: z3.array(z3.union([assessmentSectionSchema, itemRefSchema]))
4934
+ }));
4935
+ const testPartSchema = z3.object({
4936
+ identifier: z3.string().trim().min(1),
4937
+ navigationMode: z3.enum(["linear", "nonlinear"]),
4938
+ submissionMode: z3.enum(["individual", "simultaneous"]),
4939
+ ...testPartLevel,
4940
+ assessmentSections: z3.array(assessmentSectionSchema)
4941
+ });
4942
+ const assessmentTestSchema = z3.object({
4943
+ identifier: z3.string().trim().min(1),
4944
+ title: z3.string().trim().min(1).optional(),
4945
+ outcomeDeclarations: z3.array(outcomeDeclarationSchema).optional(),
4946
+ timeLimits: timeLimitsSchema.optional(),
4947
+ testParts: z3.array(testPartSchema),
4948
+ outcomeProcessing: outcomeProcessingSchema.optional(),
4949
+ testFeedbacks: z3.array(testFeedbackSchema).optional()
4950
+ });
4951
+ return { assessmentSectionSchema, testPartSchema, assessmentTestSchema };
4952
+ }
4953
+ var assessmentTestViewSchema = makeAssessmentTestSchema(assessmentItemRefViewSchema).assessmentTestSchema;
3056
4954
  export {
4955
+ timeLimitsSchema,
4956
+ testFeedbackSchema,
3057
4957
  stimulusContentFromNormalized,
4958
+ selectionSchema,
4959
+ scoreResponse,
4960
+ rpScalarSchema,
4961
+ rpExpressionSchema,
4962
+ resolveTemplate,
3058
4963
  reportItemCapability,
3059
4964
  referenceInteractionKinds,
4965
+ outcomeRuleSchema,
4966
+ outcomeProcessingSchema,
4967
+ outcomeDeclarationSchema,
4968
+ outcomeConditionBranchSchema,
4969
+ orderingSchema,
4970
+ mulberry32,
4971
+ matchTableSchema,
4972
+ matchCorrect,
4973
+ mapResponsePoint,
4974
+ mapResponse,
4975
+ makeAssessmentTestSchema,
4976
+ itemSessionControlSchema,
4977
+ interpolationTableSchema,
4978
+ foldString,
4979
+ executeTemplateProcessing,
4980
+ executeResponseProcessing,
4981
+ effectiveItemScore,
4982
+ createTestController,
4983
+ createAttemptStore,
4984
+ collectTemplateIssues,
4985
+ collectRpIssues,
4986
+ cardinalitySchema,
4987
+ branchRuleSchema,
4988
+ assessmentTestViewSchema,
3060
4989
  assessmentTestViewFromNormalized,
3061
- assessmentItemViewFromNormalized
4990
+ assessmentItemViewFromNormalized,
4991
+ assessmentItemRefViewSchema,
4992
+ applyCorrectResponseOverrides
3062
4993
  };