@intellectif/lk-core 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +4 -3
- package/dist/ai-B222dtHH.d.cts +2154 -0
- package/dist/ai-B222dtHH.d.cts.map +1 -0
- package/dist/ai-B222dtHH.d.ts +2154 -0
- package/dist/ai-B222dtHH.d.ts.map +1 -0
- package/dist/ai-check.cjs +758 -464
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +82 -68
- package/dist/ai-check.d.cts.map +1 -0
- package/dist/ai-check.d.ts +82 -68
- package/dist/ai-check.d.ts.map +1 -0
- package/dist/ai-check.js +756 -465
- package/dist/ai-check.js.map +1 -1
- package/dist/ai-writing-CCBjTOM3.js +2496 -0
- package/dist/ai-writing-CCBjTOM3.js.map +1 -0
- package/dist/ai-writing-DCVkv4Zj.cjs +2735 -0
- package/dist/ai-writing-DCVkv4Zj.cjs.map +1 -0
- package/dist/{index-DlcqkM_L.d.ts → index-BWPRPIXL.d.ts} +263 -258
- package/dist/index-BWPRPIXL.d.ts.map +1 -0
- package/dist/index-CXyOV8sm.d.cts +153 -0
- package/dist/index-CXyOV8sm.d.cts.map +1 -0
- package/dist/index-CegM8o15.d.ts +153 -0
- package/dist/index-CegM8o15.d.ts.map +1 -0
- package/dist/index-Dyu66R4-.d.cts +1071 -0
- package/dist/index-Dyu66R4-.d.cts.map +1 -0
- package/dist/{index-DlCW0XaQ.d.cts → index-O4_ExlAD.d.cts} +263 -258
- package/dist/index-O4_ExlAD.d.cts.map +1 -0
- package/dist/index-UGxRSopE.d.ts +1071 -0
- package/dist/index-UGxRSopE.d.ts.map +1 -0
- package/dist/index.cjs +1210 -1671
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1090 -853
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1090 -853
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1063 -1672
- package/dist/index.js.map +1 -1
- package/dist/item-group-DXiR8nOr.js +445 -0
- package/dist/item-group-DXiR8nOr.js.map +1 -0
- package/dist/item-group-DroFXgLb.cjs +505 -0
- package/dist/item-group-DroFXgLb.cjs.map +1 -0
- package/dist/registry-DVi0LR_i.cjs +5915 -0
- package/dist/registry-DVi0LR_i.cjs.map +1 -0
- package/dist/registry-Dsx9mNsS.js +5449 -0
- package/dist/registry-Dsx9mNsS.js.map +1 -0
- package/dist/schemas-Bdw0nGPo.js +152 -0
- package/dist/schemas-Bdw0nGPo.js.map +1 -0
- package/dist/schemas-DVbAOrtd.cjs +224 -0
- package/dist/schemas-DVbAOrtd.cjs.map +1 -0
- package/dist/schemas.cjs +16 -32
- package/dist/schemas.d.cts +2 -2
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +3 -32
- package/dist/scoring-B2_HEhMT.cjs +1748 -0
- package/dist/scoring-B2_HEhMT.cjs.map +1 -0
- package/dist/scoring-BZqUkuDF.js +1604 -0
- package/dist/scoring-BZqUkuDF.js.map +1 -0
- package/dist/scoring.cjs +37 -75
- package/dist/scoring.d.cts +3 -1103
- package/dist/scoring.d.ts +3 -1103
- package/dist/scoring.js +3 -75
- package/dist/xapi-CMTph7wG.js +332 -0
- package/dist/xapi-CMTph7wG.js.map +1 -0
- package/dist/xapi-DNzkSJ0S.cjs +362 -0
- package/dist/xapi-DNzkSJ0S.cjs.map +1 -0
- package/dist/xapi.cjs +7 -16
- package/dist/xapi.d.cts +2 -150
- package/dist/xapi.d.ts +2 -150
- package/dist/xapi.js +2 -16
- package/package.json +4 -4
- package/dist/activity-MBOYLBjR.d.cts +0 -1840
- package/dist/activity-MBOYLBjR.d.ts +0 -1840
- package/dist/chunk-2JT2WUQ2.cjs +0 -458
- package/dist/chunk-2JT2WUQ2.cjs.map +0 -1
- package/dist/chunk-DSDXL3JO.cjs +0 -4597
- package/dist/chunk-DSDXL3JO.cjs.map +0 -1
- package/dist/chunk-FBKTION6.js +0 -4597
- package/dist/chunk-FBKTION6.js.map +0 -1
- package/dist/chunk-IARLJ5MW.js +0 -1256
- package/dist/chunk-IARLJ5MW.js.map +0 -1
- package/dist/chunk-LDFQUZII.cjs +0 -950
- package/dist/chunk-LDFQUZII.cjs.map +0 -1
- package/dist/chunk-NQHETFDA.js +0 -950
- package/dist/chunk-NQHETFDA.js.map +0 -1
- package/dist/chunk-SJVRUPJT.js +0 -272
- package/dist/chunk-SJVRUPJT.js.map +0 -1
- package/dist/chunk-TJJFKKH2.cjs +0 -272
- package/dist/chunk-TJJFKKH2.cjs.map +0 -1
- package/dist/chunk-VSH7ECQL.cjs +0 -1256
- package/dist/chunk-VSH7ECQL.cjs.map +0 -1
- package/dist/chunk-XS6U2ARZ.js +0 -458
- package/dist/chunk-XS6U2ARZ.js.map +0 -1
- package/dist/schemas.cjs.map +0 -1
- package/dist/schemas.js.map +0 -1
- package/dist/scoring.cjs.map +0 -1
- package/dist/scoring.js.map +0 -1
- package/dist/xapi.cjs.map +0 -1
- package/dist/xapi.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,1771 +1,1310 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
var _chunk2JT2WUQ2cjs = require('./chunk-2JT2WUQ2.cjs');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
var _chunkLDFQUZIIcjs = require('./chunk-LDFQUZII.cjs');
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
var _chunkVSH7ECQLcjs = require('./chunk-VSH7ECQL.cjs');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
var _chunkDSDXL3JOcjs = require('./chunk-DSDXL3JO.cjs');
|
|
126
|
-
|
|
127
|
-
// src/content-hash.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_registry = require("./registry-DVi0LR_i.cjs");
|
|
3
|
+
const require_ai_writing = require("./ai-writing-DCVkv4Zj.cjs");
|
|
4
|
+
const require_scoring = require("./scoring-B2_HEhMT.cjs");
|
|
5
|
+
const require_item_group = require("./item-group-DroFXgLb.cjs");
|
|
6
|
+
const require_schemas = require("./schemas-DVbAOrtd.cjs");
|
|
7
|
+
const require_xapi = require("./xapi-DNzkSJ0S.cjs");
|
|
8
|
+
//#region src/content-hash.ts
|
|
9
|
+
/**
|
|
10
|
+
* Content fingerprinting — "is this the same content the learner was served?"
|
|
11
|
+
*
|
|
12
|
+
* A recorded attempt outlives the content it was taken against. Papers get
|
|
13
|
+
* corrected, a typo is fixed, an option is reworded — and six months later a
|
|
14
|
+
* remark, an appeal, or a per-item analysis is run against content that is no
|
|
15
|
+
* longer what the learner saw. Nothing warns anybody, because the ids all still
|
|
16
|
+
* match. A fingerprint stored with the attempt turns that silent drift into a
|
|
17
|
+
* question somebody can answer.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic JSON: object keys sorted, arrays left in order, `undefined`
|
|
21
|
+
* omitted from objects (matching `JSON.stringify`) and rendered as `null`
|
|
22
|
+
* inside arrays.
|
|
23
|
+
*
|
|
24
|
+
* Key order is the whole point. Two objects that differ only in the order
|
|
25
|
+
* their keys happened to be written are the SAME content, and a fingerprint
|
|
26
|
+
* that disagreed would raise a false alarm every time a payload made a
|
|
27
|
+
* round-trip through a different serializer.
|
|
28
|
+
*/
|
|
128
29
|
function canonicalJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
const source = value;
|
|
153
|
-
const parts = [];
|
|
154
|
-
for (const key of Object.keys(source).sort()) {
|
|
155
|
-
const entry = source[key];
|
|
156
|
-
if (entry === void 0) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
parts.push(`${JSON.stringify(key)}:${canonicalJson(entry, seen)}`);
|
|
160
|
-
}
|
|
161
|
-
return `{${parts.join(",")}}`;
|
|
162
|
-
} finally {
|
|
163
|
-
seen.delete(value);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return "null";
|
|
30
|
+
if (value === null) return "null";
|
|
31
|
+
if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value === 0 ? 0 : value) : `"#${String(value)}"`;
|
|
32
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
33
|
+
if (typeof value === "bigint") return `"#${value.toString()}n"`;
|
|
34
|
+
if (typeof value === "object") {
|
|
35
|
+
if (seen.has(value)) throw new Error("canonicalJson: the value contains a circular reference and cannot be fingerprinted.");
|
|
36
|
+
seen.add(value);
|
|
37
|
+
try {
|
|
38
|
+
if (Array.isArray(value)) return `[${value.map((element) => element === void 0 ? "null" : canonicalJson(element, seen)).join(",")}]`;
|
|
39
|
+
const source = value;
|
|
40
|
+
const parts = [];
|
|
41
|
+
for (const key of Object.keys(source).sort()) {
|
|
42
|
+
const entry = source[key];
|
|
43
|
+
if (entry === void 0) continue;
|
|
44
|
+
parts.push(`${JSON.stringify(key)}:${canonicalJson(entry, seen)}`);
|
|
45
|
+
}
|
|
46
|
+
return `{${parts.join(",")}}`;
|
|
47
|
+
} finally {
|
|
48
|
+
seen.delete(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "null";
|
|
167
52
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
53
|
+
const FNV_OFFSET = 14695981039346656037n;
|
|
54
|
+
const FNV_PRIME = 1099511628211n;
|
|
55
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
56
|
+
/**
|
|
57
|
+
* FNV-1a over the UTF-8 bytes of `input`, as 16 lowercase hex digits.
|
|
58
|
+
*
|
|
59
|
+
* **This is a change-detection fingerprint, not a tamper-evident signature.**
|
|
60
|
+
* It is deterministic across runtimes and dependency-free, which is what makes
|
|
61
|
+
* it usable in a stored grade record — but an adversary who can edit content
|
|
62
|
+
* can also, with effort, preserve the fingerprint. If you need the stronger
|
|
63
|
+
* property, sign the plan with a key the content author does not hold; this
|
|
64
|
+
* exists to catch honest edits, which is what actually happens.
|
|
65
|
+
*/
|
|
171
66
|
function fingerprint(input) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
return hash.toString(16).padStart(16, "0");
|
|
67
|
+
const bytes = new TextEncoder().encode(input);
|
|
68
|
+
let hash = FNV_OFFSET;
|
|
69
|
+
for (const byte of bytes) hash = (hash ^ BigInt(byte)) * FNV_PRIME & MASK64;
|
|
70
|
+
return hash.toString(16).padStart(16, "0");
|
|
178
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Fingerprints any JSON-serialisable value through {@link canonicalJson}, so
|
|
74
|
+
* the result depends on the content and not on how it was written.
|
|
75
|
+
*/
|
|
179
76
|
function contentHash(value) {
|
|
180
|
-
|
|
77
|
+
return fingerprint(canonicalJson(value));
|
|
181
78
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/delivery.ts
|
|
81
|
+
/**
|
|
82
|
+
* The policy that restricts nothing: every setting `true`. It is what an absent
|
|
83
|
+
* policy resolves to, and it is exactly the behaviour the SDK had before
|
|
84
|
+
* delivery policies existed.
|
|
85
|
+
*/
|
|
86
|
+
const OPEN_DELIVERY_POLICY = Object.freeze({
|
|
87
|
+
feedback: true,
|
|
88
|
+
solutions: true,
|
|
89
|
+
hints: true,
|
|
90
|
+
ai: Object.freeze({
|
|
91
|
+
hints: true,
|
|
92
|
+
explanations: true
|
|
93
|
+
})
|
|
189
94
|
});
|
|
190
|
-
|
|
191
|
-
|
|
95
|
+
const SETTINGS = [
|
|
96
|
+
"feedback",
|
|
97
|
+
"solutions",
|
|
98
|
+
"hints"
|
|
99
|
+
];
|
|
100
|
+
const AI_SETTINGS = ["hints", "explanations"];
|
|
101
|
+
/**
|
|
102
|
+
* One setting as a render reads it. Absent or `null` is unset, and unset is
|
|
103
|
+
* `true`: JSON has no `undefined`, so a setting nobody chose arrives from a
|
|
104
|
+
* database as `null`.
|
|
105
|
+
*
|
|
106
|
+
* Anything else that is not exactly `true` reads as `false`. Every setting is a
|
|
107
|
+
* restriction, so a value that cannot be read is read as the restriction — a
|
|
108
|
+
* `"false"` string from a form, say, whose author meant exactly that. Reading
|
|
109
|
+
* it as `true` would put hints on a paper whose school had switched them off.
|
|
110
|
+
*/
|
|
192
111
|
function allows(value) {
|
|
193
|
-
|
|
112
|
+
return value === void 0 || value === null || value === true;
|
|
194
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* A policy with every setting spelled out — see {@link DeliveryPolicy}.
|
|
116
|
+
*
|
|
117
|
+
* Tolerant, because it runs where a learner is waiting: nothing here throws.
|
|
118
|
+
* A value it cannot read restricts rather than allows (see above), and a
|
|
119
|
+
* policy that is not an object at all restricts everything. Check a policy
|
|
120
|
+
* before you store it with {@link validateDeliveryPolicy}; `planAttempt` does.
|
|
121
|
+
*/
|
|
195
122
|
function resolveDeliveryPolicy(policy) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
123
|
+
if (policy === void 0 || policy === null) return OPEN_DELIVERY_POLICY;
|
|
124
|
+
if (typeof policy !== "object" || Array.isArray(policy)) return {
|
|
125
|
+
feedback: false,
|
|
126
|
+
solutions: false,
|
|
127
|
+
hints: false,
|
|
128
|
+
ai: {
|
|
129
|
+
hints: false,
|
|
130
|
+
explanations: false
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const given = policy;
|
|
134
|
+
const ai = given.ai;
|
|
135
|
+
const aiGiven = ai === void 0 || ai === null || ai === true ? {} : typeof ai === "object" && !Array.isArray(ai) ? ai : {
|
|
136
|
+
hints: false,
|
|
137
|
+
explanations: false
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
feedback: allows(given.feedback),
|
|
141
|
+
solutions: allows(given.solutions),
|
|
142
|
+
hints: allows(given.hints),
|
|
143
|
+
ai: {
|
|
144
|
+
hints: allows(aiGiven.hints),
|
|
145
|
+
explanations: allows(aiGiven.explanations)
|
|
146
|
+
}
|
|
147
|
+
};
|
|
216
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The strictest of several policies: a setting is on only where every one of
|
|
151
|
+
* them leaves it on. How a course's policy and an assessment's combine, and
|
|
152
|
+
* how a pager's policy wins over whatever a question it draws was handed.
|
|
153
|
+
*/
|
|
217
154
|
function combineDeliveryPolicies(...policies) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
155
|
+
const resolved = policies.map(resolveDeliveryPolicy);
|
|
156
|
+
const every = (read) => resolved.every(read);
|
|
157
|
+
return {
|
|
158
|
+
feedback: every((one) => one.feedback),
|
|
159
|
+
solutions: every((one) => one.solutions),
|
|
160
|
+
hints: every((one) => one.hints),
|
|
161
|
+
ai: {
|
|
162
|
+
hints: every((one) => one.ai.hints),
|
|
163
|
+
explanations: every((one) => one.ai.explanations)
|
|
164
|
+
}
|
|
165
|
+
};
|
|
229
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Checks a policy before it is stored: each setting `true`, `false`, `null`
|
|
169
|
+
* or absent, and nothing else — no misspelled setting, no string standing in
|
|
170
|
+
* for a boolean.
|
|
171
|
+
*
|
|
172
|
+
* Stricter than {@link resolveDeliveryPolicy} on purpose. A misspelled
|
|
173
|
+
* restriction (`hint: false`) is silently no restriction at all, and a
|
|
174
|
+
* policy is a record of the conditions a paper was sat under: it is worth
|
|
175
|
+
* refusing before it is written down, rather than read charitably after.
|
|
176
|
+
*/
|
|
230
177
|
function validateDeliveryPolicy(policy) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
178
|
+
const issues = [];
|
|
179
|
+
if (policy === void 0 || policy === null) return {
|
|
180
|
+
success: true,
|
|
181
|
+
data: {}
|
|
182
|
+
};
|
|
183
|
+
if (typeof policy !== "object" || Array.isArray(policy)) return {
|
|
184
|
+
success: false,
|
|
185
|
+
issues: [{
|
|
186
|
+
path: "",
|
|
187
|
+
message: "A delivery policy is an object of settings."
|
|
188
|
+
}]
|
|
189
|
+
};
|
|
190
|
+
const given = policy;
|
|
191
|
+
for (const key of Object.keys(given)) {
|
|
192
|
+
if (key === "ai") continue;
|
|
193
|
+
if (!SETTINGS.includes(key)) {
|
|
194
|
+
issues.push({
|
|
195
|
+
path: key,
|
|
196
|
+
message: `"${key}" is not a delivery setting. The settings are feedback, solutions, hints and ai.`
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const value = given[key];
|
|
201
|
+
if (value !== true && value !== false && value !== null && value !== void 0) issues.push({
|
|
202
|
+
path: key,
|
|
203
|
+
message: `"${key}" must be true, false or null.`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const ai = given.ai;
|
|
207
|
+
if (ai !== void 0 && ai !== null && ai !== true && ai !== false) {
|
|
208
|
+
if (typeof ai !== "object" || Array.isArray(ai)) issues.push({
|
|
209
|
+
path: "ai",
|
|
210
|
+
message: "\"ai\" must be true, false, or an object of hints and explanations."
|
|
211
|
+
});
|
|
212
|
+
else for (const [key, value] of Object.entries(ai)) {
|
|
213
|
+
if (!AI_SETTINGS.includes(key)) {
|
|
214
|
+
issues.push({
|
|
215
|
+
path: `ai.${key}`,
|
|
216
|
+
message: `"ai.${key}" is not an AI setting. The settings are hints and explanations.`
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (value !== true && value !== false && value !== null && value !== void 0) issues.push({
|
|
221
|
+
path: `ai.${key}`,
|
|
222
|
+
message: `"ai.${key}" must be true, false or null.`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return issues.length > 0 ? {
|
|
227
|
+
success: false,
|
|
228
|
+
issues
|
|
229
|
+
} : {
|
|
230
|
+
success: true,
|
|
231
|
+
data: given
|
|
232
|
+
};
|
|
281
233
|
}
|
|
282
|
-
|
|
283
|
-
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/shuffle.ts
|
|
236
|
+
/**
|
|
237
|
+
* Seeded, deterministic shuffling — the ONE algorithm every presentation-order
|
|
238
|
+
* decision in the SDK goes through: option order in a multiple-choice item,
|
|
239
|
+
* item order inside an item group, entry order in a sequence. A server and a
|
|
240
|
+
* client holding the same seed derive the same order, and an order recorded
|
|
241
|
+
* against an attempt can be rebuilt later from nothing but its seed.
|
|
242
|
+
*
|
|
243
|
+
* STABILITY: the hash and the generator are part of the wire contract. A
|
|
244
|
+
* stored attempt may hold only a seed and rely on this function to reproduce
|
|
245
|
+
* the order it presented; changing either would silently re-order every
|
|
246
|
+
* recorded attempt. Any change here is a package major, and the pinned
|
|
247
|
+
* permutation test exists so an accidental one fails loudly.
|
|
248
|
+
*/
|
|
249
|
+
/** FNV-1a hash of a string → unsigned 32-bit integer. */
|
|
284
250
|
function hashSeed(input) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
251
|
+
let h = 2166136261;
|
|
252
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
253
|
+
h ^= input.charCodeAt(i);
|
|
254
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
255
|
+
}
|
|
256
|
+
return h;
|
|
291
257
|
}
|
|
258
|
+
/** LCG-driven Fisher–Yates over a 32-bit seed; returns a permutation (no loss, no duplicate). */
|
|
292
259
|
function shuffleWithSeed(items, seed, version) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
260
|
+
const out = [...items];
|
|
261
|
+
let s = seed >>> 0 || 1;
|
|
262
|
+
for (let i = out.length - 1; i > 0; i -= 1) {
|
|
263
|
+
s = Math.imul(s, 1664525) + 1013904223 >>> 0;
|
|
264
|
+
const j = version === 2 ? Math.floor(s / 4294967296 * (i + 1)) : s % (i + 1);
|
|
265
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
301
268
|
}
|
|
302
269
|
function seededShuffle(items, seed, options = {}) {
|
|
303
|
-
|
|
270
|
+
return shuffleWithSeed(items, hashSeed(seed), options.version ?? 1);
|
|
304
271
|
}
|
|
305
|
-
|
|
306
|
-
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/item-group.ts
|
|
274
|
+
/** Narrows a sequence entry to an item group. */
|
|
307
275
|
function isItemGroup(entry) {
|
|
308
|
-
|
|
276
|
+
return entry.type === "item-group";
|
|
309
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* An entry's declared `slotKey`, if it has one. Read structurally rather than
|
|
280
|
+
* from the type, because a plain activity may carry one too — `slotKey` is a
|
|
281
|
+
* property of an item's PLACE in a paper, so any entry can declare it without
|
|
282
|
+
* every activity schema having to know about assessment assembly.
|
|
283
|
+
*
|
|
284
|
+
* A key containing `.` is rejected: `.` separates a group from its item in a
|
|
285
|
+
* slot id, so `"a.b"` as a group key would be indistinguishable from item `b`
|
|
286
|
+
* of group `a`.
|
|
287
|
+
*/
|
|
310
288
|
function keyOf(entry) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
throw new Error(
|
|
317
|
-
`flattenSequence: slotKey must be a non-empty string, received ${String(key)}.`
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
if (key.includes(".")) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`flattenSequence: slotKey "${key}" contains a "." , which separates a group from its item in a slot id. Choose a key without it, or the two become indistinguishable.`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
return key;
|
|
289
|
+
const key = entry.slotKey;
|
|
290
|
+
if (key === void 0) return;
|
|
291
|
+
if (typeof key !== "string" || key.length === 0) throw new Error(`flattenSequence: slotKey must be a non-empty string, received ${String(key)}.`);
|
|
292
|
+
if (key.includes(".")) throw new Error(`flattenSequence: slotKey "${key}" contains a "." , which separates a group from its item in a slot id. Choose a key without it, or the two become indistinguishable.`);
|
|
293
|
+
return key;
|
|
326
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* An interactive video's items in the order its quizzes open: by the quiz's
|
|
297
|
+
* time — stably, so quizzes at one moment keep their authored order — then by
|
|
298
|
+
* position within the quiz. An item no quiz places (content the schema would
|
|
299
|
+
* refuse) keeps its authored place after every placed one, rather than
|
|
300
|
+
* vanishing from the paper.
|
|
301
|
+
*/
|
|
327
302
|
function inQuizOrder(items, timeline) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
303
|
+
const placement = /* @__PURE__ */ new Map();
|
|
304
|
+
timeline.cues.forEach((cue, cueIndex) => {
|
|
305
|
+
cue.itemIds.forEach((itemId, position) => {
|
|
306
|
+
if (!placement.has(itemId)) placement.set(itemId, {
|
|
307
|
+
at: cue.at,
|
|
308
|
+
cue: cueIndex,
|
|
309
|
+
position
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
const unplaced = {
|
|
314
|
+
at: Number.POSITIVE_INFINITY,
|
|
315
|
+
cue: Number.POSITIVE_INFINITY,
|
|
316
|
+
position: 0
|
|
317
|
+
};
|
|
318
|
+
return items.map((entry) => ({
|
|
319
|
+
entry,
|
|
320
|
+
place: placement.get(entry.item.id) ?? unplaced
|
|
321
|
+
})).sort((a, b) => a.place.at - b.place.at || a.place.cue - b.place.cue || a.place.position - b.place.position || a.entry.itemIndex - b.entry.itemIndex).map(({ entry }) => entry);
|
|
340
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Turns a sequence definition — loose activities and item groups, in authored
|
|
325
|
+
* order — into the ordered list of slots to present. Pure and deterministic:
|
|
326
|
+
* call it on the server to record an attempt's order, and on the client to
|
|
327
|
+
* render it, and the two agree.
|
|
328
|
+
*
|
|
329
|
+
* Shuffling is opt-in and seeded. Groups are shuffle-atomic: with
|
|
330
|
+
* `shuffleEntries` a group changes position but stays one contiguous block,
|
|
331
|
+
* and only a group that declares `shuffle: 'within-group'` has its items
|
|
332
|
+
* reordered. Every slot carries a `slotId` derived from the authored
|
|
333
|
+
* position, so identities survive shuffling.
|
|
334
|
+
*
|
|
335
|
+
* @throws Error when a shuffle is requested without a `seed`.
|
|
336
|
+
*/
|
|
341
337
|
function flattenSequence(entries, options = {}) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
seenSlotIds.add(slot.slotId);
|
|
402
|
-
}
|
|
403
|
-
for (const { entry, entryIndex } of ordered) {
|
|
404
|
-
const entryKey = _nullishCoalesce(keyOf(entry), () => ( String(entryIndex)));
|
|
405
|
-
if (seenEntryKeys.has(entryKey)) {
|
|
406
|
-
throw new Error(
|
|
407
|
-
`flattenSequence: duplicate entry key "${entryKey}". A loose activity and an item group cannot share a slotKey \u2014 their slot ids would not collide, but everything that reads the entry they belong to would treat them as one entry.`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
seenEntryKeys.add(entryKey);
|
|
411
|
-
}
|
|
412
|
-
return slots;
|
|
338
|
+
const shuffleEntries = options.shuffleEntries === true;
|
|
339
|
+
if ((shuffleEntries || entries.some((entry) => isItemGroup(entry) && entry.shuffle === "within-group")) && options.seed === void 0) throw new Error("flattenSequence: a seed is required when shuffling (shuffleEntries, or a group with shuffle: \"within-group\"). Pass the attempt id so the server and the client derive the same order.");
|
|
340
|
+
const seed = options.seed ?? "";
|
|
341
|
+
const authored = entries.map((entry, entryIndex) => ({
|
|
342
|
+
entry,
|
|
343
|
+
entryIndex
|
|
344
|
+
}));
|
|
345
|
+
const ordered = shuffleEntries ? seededShuffle(authored, `${seed}:entries`, { version: 1 }) : authored;
|
|
346
|
+
const slots = [];
|
|
347
|
+
for (const { entry, entryIndex } of ordered) {
|
|
348
|
+
const entryKey = keyOf(entry) ?? String(entryIndex);
|
|
349
|
+
if (!isItemGroup(entry)) {
|
|
350
|
+
slots.push({
|
|
351
|
+
slotId: entryKey,
|
|
352
|
+
index: slots.length,
|
|
353
|
+
activity: entry
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (entry.items.length === 0) throw new Error(`flattenSequence: item group "${entry.id}" has no items. An empty group would silently remove its stimulus and its questions from the presented sequence.`);
|
|
358
|
+
const timeline = entry.timeline;
|
|
359
|
+
if (timeline !== void 0 && entry.shuffle === "within-group") throw new Error(`flattenSequence: item group "${entry.id}" has a timeline and shuffle: "within-group". An interactive video presents its questions in the order its quizzes open.`);
|
|
360
|
+
const items = entry.items.map((item, itemIndex) => ({
|
|
361
|
+
item,
|
|
362
|
+
itemIndex
|
|
363
|
+
}));
|
|
364
|
+
const presented = timeline !== void 0 ? inQuizOrder(items, timeline) : entry.shuffle === "within-group" ? seededShuffle(items, `${seed}:group:${entry.id}`, { version: 1 }) : items;
|
|
365
|
+
const size = presented.length;
|
|
366
|
+
presented.forEach(({ item, itemIndex }, position) => {
|
|
367
|
+
const cue = timeline?.cues.find((candidate) => candidate.itemIds.includes(item.id));
|
|
368
|
+
slots.push({
|
|
369
|
+
slotId: `${entryKey}.${keyOf(item) ?? String(itemIndex)}`,
|
|
370
|
+
index: slots.length,
|
|
371
|
+
activity: item,
|
|
372
|
+
group: {
|
|
373
|
+
id: entry.id,
|
|
374
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
375
|
+
stimulus: entry.stimulus,
|
|
376
|
+
position,
|
|
377
|
+
size,
|
|
378
|
+
...timeline !== void 0 ? { timeline } : {},
|
|
379
|
+
...cue !== void 0 ? { cue } : {}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const seenSlotIds = /* @__PURE__ */ new Set();
|
|
385
|
+
const seenEntryKeys = /* @__PURE__ */ new Set();
|
|
386
|
+
for (const slot of slots) {
|
|
387
|
+
if (seenSlotIds.has(slot.slotId)) throw new Error(`flattenSequence: duplicate slot id "${slot.slotId}". Two entries declare the same slotKey, so responses stored against them could not be told apart.`);
|
|
388
|
+
seenSlotIds.add(slot.slotId);
|
|
389
|
+
}
|
|
390
|
+
for (const { entry, entryIndex } of ordered) {
|
|
391
|
+
const entryKey = keyOf(entry) ?? String(entryIndex);
|
|
392
|
+
if (seenEntryKeys.has(entryKey)) throw new Error(`flattenSequence: duplicate entry key "${entryKey}". A loose activity and an item group cannot share a slotKey — their slot ids would not collide, but everything that reads the entry they belong to would treat them as one entry.`);
|
|
393
|
+
seenEntryKeys.add(entryKey);
|
|
394
|
+
}
|
|
395
|
+
return slots;
|
|
413
396
|
}
|
|
414
|
-
|
|
415
|
-
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/media-budget.ts
|
|
399
|
+
/**
|
|
400
|
+
* The authored-entry prefix of a slot id: `"3"` for both `"3"` and `"3.1"`.
|
|
401
|
+
*
|
|
402
|
+
* Safe because every slot id in a plan is produced by `flattenSequence`, and
|
|
403
|
+
* `keyOf` rejects a `.` in both an entry key and an item key — so the first
|
|
404
|
+
* `.` is always the group/item separator.
|
|
405
|
+
*/
|
|
416
406
|
function entryKeyOf(slotId) {
|
|
417
|
-
|
|
418
|
-
|
|
407
|
+
const dot = slotId.indexOf(".");
|
|
408
|
+
return dot === -1 ? slotId : slotId.slice(0, dot);
|
|
419
409
|
}
|
|
410
|
+
/** Budget key for an activity's OWN `data.media`. One budget per slot. */
|
|
420
411
|
function slotMediaKey(slotId) {
|
|
421
|
-
|
|
412
|
+
return `slot:${slotId}`;
|
|
422
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Budget key for an item group's `stimulus.media`.
|
|
416
|
+
*
|
|
417
|
+
* Keyed by the ENTRY, not the slot: one recording serves every question in the
|
|
418
|
+
* group, so `"3.0"`…`"3.5"` share one budget. Keying by `slotId` would hand a
|
|
419
|
+
* six-question `maxPlays: 2` listening group twelve plays; keying by
|
|
420
|
+
* `stimulus.id` would collide across papers, which is the thing slot keys and
|
|
421
|
+
* `planHash` exist to prevent.
|
|
422
|
+
*/
|
|
423
423
|
function stimulusMediaKey(slotId) {
|
|
424
|
-
|
|
424
|
+
return `stimulus:${entryKeyOf(slotId)}`;
|
|
425
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Resolves the defaults once, so the schema refinements, the plan and the
|
|
428
|
+
* renderer cannot drift apart about what a policy means.
|
|
429
|
+
*
|
|
430
|
+
* Absent `playback` resolves to exactly the SDK's pre-0.8.0 behaviour:
|
|
431
|
+
* `controls: 'native'`, no budget, free seeking, free speed, no hints.
|
|
432
|
+
*/
|
|
426
433
|
function resolvePlaybackPolicy(media) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
};
|
|
434
|
+
const p = media.playback;
|
|
435
|
+
const budgeted = typeof p?.maxPlays === "number";
|
|
436
|
+
const seek = p?.seek ?? (budgeted ? "none" : "allow");
|
|
437
|
+
const rate = p?.rate ?? "allow";
|
|
438
|
+
return {
|
|
439
|
+
controls: p?.controls ?? (budgeted || seek === "none" || rate === "fixed" ? "minimal" : "native"),
|
|
440
|
+
maxPlays: budgeted ? p?.maxPlays : null,
|
|
441
|
+
seek,
|
|
442
|
+
rate,
|
|
443
|
+
nativeControlHints: p?.nativeControlHints ?? []
|
|
444
|
+
};
|
|
439
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Every budgeted recording a plan contains: budget key → the `maxPlays` that
|
|
448
|
+
* was frozen when the attempt was planned.
|
|
449
|
+
*
|
|
450
|
+
* Use it to build the storage rows an attempt needs, and to check a stored
|
|
451
|
+
* ledger against the paper it claims to belong to.
|
|
452
|
+
*/
|
|
440
453
|
function planMediaBudgets(plan) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
out[budget.key] = budget.maxPlays;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return out;
|
|
454
|
+
const out = {};
|
|
455
|
+
for (const slot of plan.slots) for (const budget of slot.mediaBudgets ?? []) out[budget.key] = budget.maxPlays;
|
|
456
|
+
return out;
|
|
448
457
|
}
|
|
449
458
|
function assertEntry(key, entry) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
`serializeMediaPlayLedger: play count for ${JSON.stringify(key)} is ${JSON.stringify(_optionalChain([entry, 'optionalAccess', _10 => _10.plays]))}. A count must be a non-negative integer; Number(a NULL column) is the usual cause.`
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
if (entry.at !== void 0 && (!Number.isFinite(entry.at) || entry.at < 0)) {
|
|
456
|
-
throw new Error(
|
|
457
|
-
`serializeMediaPlayLedger: position for ${JSON.stringify(key)} is ${JSON.stringify(entry.at)}. A position must be a finite number of seconds >= 0.`
|
|
458
|
-
);
|
|
459
|
-
}
|
|
459
|
+
if (entry === null || typeof entry !== "object" || !Number.isInteger(entry.plays) || entry.plays < 0) throw new Error(`serializeMediaPlayLedger: play count for ${JSON.stringify(key)} is ${JSON.stringify(entry?.plays)}. A count must be a non-negative integer; Number(a NULL column) is the usual cause.`);
|
|
460
|
+
if (entry.at !== void 0 && (!Number.isFinite(entry.at) || entry.at < 0)) throw new Error(`serializeMediaPlayLedger: position for ${JSON.stringify(key)} is ${JSON.stringify(entry.at)}. A position must be a finite number of seconds >= 0.`);
|
|
460
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Validates a set of spent-play counts against the plan that budgeted them,
|
|
464
|
+
* and stamps the envelope.
|
|
465
|
+
*
|
|
466
|
+
* Validates on the way **in**: a play recorded against a recording the paper
|
|
467
|
+
* does not budget is a bug at the moment it is written, and discovering it
|
|
468
|
+
* when a learner tries to resume is discovering it far too late.
|
|
469
|
+
*
|
|
470
|
+
* @throws Error when a key is not budgeted by the plan, or a count is not a
|
|
471
|
+
* non-negative integer, or a position is not a finite number.
|
|
472
|
+
*/
|
|
461
473
|
function serializeMediaPlayLedger(plan, entries, options = {}) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
entries: out,
|
|
481
|
-
...options.savedAt !== void 0 ? { savedAt: options.savedAt } : {}
|
|
482
|
-
};
|
|
474
|
+
if (entries === null || typeof entries !== "object") throw new Error("serializeMediaPlayLedger: entries must be an object keyed by media key.");
|
|
475
|
+
const budgets = planMediaBudgets(plan);
|
|
476
|
+
const out = {};
|
|
477
|
+
for (const key of Object.keys(entries)) {
|
|
478
|
+
if (!Object.hasOwn(budgets, key)) throw new Error(`serializeMediaPlayLedger: media play recorded against a recording the plan does not budget: ${JSON.stringify(key)}. Keys come from slotMediaKey() / stimulusMediaKey(); a key that belongs to no budgeted media means the client and the plan disagree about which paper this is.`);
|
|
479
|
+
const entry = entries[key];
|
|
480
|
+
assertEntry(key, entry);
|
|
481
|
+
out[key] = {
|
|
482
|
+
plays: entry.plays,
|
|
483
|
+
...entry.at !== void 0 ? { at: entry.at } : {}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
ledgerVersion: "1.0",
|
|
488
|
+
planHash: plan.planHash,
|
|
489
|
+
entries: out,
|
|
490
|
+
...options.savedAt !== void 0 ? { savedAt: options.savedAt } : {}
|
|
491
|
+
};
|
|
483
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Reopens a stored ledger against the paper it was spent on.
|
|
495
|
+
*
|
|
496
|
+
* Refuses a ledger from a different paper for the same reason
|
|
497
|
+
* `restoreAttemptState` does: budget keys are short and repeat across papers
|
|
498
|
+
* (`slot:0`, `stimulus:1`), so a ledger from last term's midterm would hold a
|
|
499
|
+
* learner to a budget from an exam they never sat, and look entirely plausible
|
|
500
|
+
* doing it.
|
|
501
|
+
*
|
|
502
|
+
* @throws Error on an unrecognised `ledgerVersion`, a `planHash` mismatch, or
|
|
503
|
+
* anything `serializeMediaPlayLedger` refuses.
|
|
504
|
+
*/
|
|
484
505
|
function restoreMediaPlayLedger(plan, stored) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
`restoreMediaPlayLedger: unsupported ledgerVersion ${JSON.stringify(stored.ledgerVersion)}. This build understands "1.0"; a newer ledger must be migrated before it is restored.`
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
if (stored.entries === null || typeof stored.entries !== "object") {
|
|
494
|
-
throw new Error(
|
|
495
|
-
"restoreMediaPlayLedger: the ledger carries no entries object (a NULL column, or a partially written row)."
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
if (stored.planHash !== plan.planHash) {
|
|
499
|
-
throw new Error(
|
|
500
|
-
`restoreMediaPlayLedger: this ledger belongs to a different paper (ledger ${stored.planHash}, plan ${plan.planHash}). Restoring it would hold the learner to a play budget from an exam they never sat.`
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
return serializeMediaPlayLedger(plan, stored.entries, {
|
|
504
|
-
...stored.savedAt !== void 0 ? { savedAt: stored.savedAt } : {}
|
|
505
|
-
});
|
|
506
|
+
if (stored === null || typeof stored !== "object") throw new Error("restoreMediaPlayLedger: the stored ledger is not an object.");
|
|
507
|
+
if (stored.ledgerVersion !== "1.0") throw new Error(`restoreMediaPlayLedger: unsupported ledgerVersion ${JSON.stringify(stored.ledgerVersion)}. This build understands "1.0"; a newer ledger must be migrated before it is restored.`);
|
|
508
|
+
if (stored.entries === null || typeof stored.entries !== "object") throw new Error("restoreMediaPlayLedger: the ledger carries no entries object (a NULL column, or a partially written row).");
|
|
509
|
+
if (stored.planHash !== plan.planHash) throw new Error(`restoreMediaPlayLedger: this ledger belongs to a different paper (ledger ${stored.planHash}, plan ${plan.planHash}). Restoring it would hold the learner to a play budget from an exam they never sat.`);
|
|
510
|
+
return serializeMediaPlayLedger(plan, stored.entries, { ...stored.savedAt !== void 0 ? { savedAt: stored.savedAt } : {} });
|
|
506
511
|
}
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/attempt-plan.ts
|
|
514
|
+
/**
|
|
515
|
+
* The activity as CONTENT, with its slot key removed.
|
|
516
|
+
*
|
|
517
|
+
* `slotKey` rides on the same object but is identity, not content — the whole
|
|
518
|
+
* point of the design. Including it in the fingerprint meant that annotating
|
|
519
|
+
* an existing entry with the key that pins its identity reported as
|
|
520
|
+
* "this question was edited", which is precisely backwards.
|
|
521
|
+
*/
|
|
509
522
|
function contentOf(activity) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const { slotKey: _slotKey, ...content } = activity;
|
|
514
|
-
return content;
|
|
523
|
+
if (!Object.hasOwn(activity, "slotKey")) return activity;
|
|
524
|
+
const { slotKey: _slotKey, ...content } = activity;
|
|
525
|
+
return content;
|
|
515
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* The budgeted recordings a slot presents: its own media, and its group's
|
|
529
|
+
* stimulus. Returns `undefined` — not `[]` — when nothing is budgeted, so the
|
|
530
|
+
* conditional spread below leaves the serialized slot byte-identical to what
|
|
531
|
+
* every pre-0.8.0 paper produced.
|
|
532
|
+
*/
|
|
516
533
|
function mediaBudgetsOf(slot) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
534
|
+
const budgets = [];
|
|
535
|
+
const own = slot.activity.media;
|
|
536
|
+
if (own !== void 0) {
|
|
537
|
+
const maxPlays = resolvePlaybackPolicy(own).maxPlays;
|
|
538
|
+
if (maxPlays !== null) budgets.push({
|
|
539
|
+
key: slotMediaKey(slot.slotId),
|
|
540
|
+
maxPlays
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const stimulusMedia = slot.group?.stimulus.media;
|
|
544
|
+
if (stimulusMedia !== void 0) {
|
|
545
|
+
const maxPlays = resolvePlaybackPolicy(stimulusMedia).maxPlays;
|
|
546
|
+
if (maxPlays !== null) budgets.push({
|
|
547
|
+
key: stimulusMediaKey(slot.slotId),
|
|
548
|
+
maxPlays
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return budgets.length > 0 ? budgets : void 0;
|
|
533
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Refuses a paper in which one recording is budgeted under several keys.
|
|
555
|
+
*
|
|
556
|
+
* Six questions that each carry the same `/audio/part2.mp3` with `maxPlays: 2`
|
|
557
|
+
* are six budgets, so the learner gets twelve plays of one recording while the
|
|
558
|
+
* paper says two. The fix is authoring, not arithmetic: questions that share a
|
|
559
|
+
* recording belong in an item group, whose stimulus is one recording with one
|
|
560
|
+
* budget.
|
|
561
|
+
*/
|
|
534
562
|
function assertNoSplitBudgets(slots) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
for (const [url, keys] of keysByUrl) {
|
|
546
|
-
if (keys.length > 1) {
|
|
547
|
-
throw new Error(
|
|
548
|
-
`planAttempt: media ${JSON.stringify(url)} is budgeted under ${keys.length} separate keys (${keys.join(", ")}), so one recording grants ${keys.length} \xD7 maxPlays. Put the questions that share a recording in an item group \u2014 a group's stimulus is one recording with one budget.`
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
563
|
+
const keysByUrl = /* @__PURE__ */ new Map();
|
|
564
|
+
for (const slot of slots) {
|
|
565
|
+
const own = slot.activity.media;
|
|
566
|
+
if (own === void 0 || resolvePlaybackPolicy(own).maxPlays === null) continue;
|
|
567
|
+
const keys = keysByUrl.get(own.url) ?? [];
|
|
568
|
+
keys.push(slotMediaKey(slot.slotId));
|
|
569
|
+
keysByUrl.set(own.url, keys);
|
|
570
|
+
}
|
|
571
|
+
for (const [url, keys] of keysByUrl) if (keys.length > 1) throw new Error(`planAttempt: media ${JSON.stringify(url)} is budgeted under ${keys.length} separate keys (${keys.join(", ")}), so one recording grants ${keys.length} × maxPlays. Put the questions that share a recording in an item group — a group's stimulus is one recording with one budget.`);
|
|
552
572
|
}
|
|
553
573
|
function planSlot(slot, points) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
at: slot.group.cue.at,
|
|
576
|
-
...slot.group.cue.required !== void 0 ? { required: slot.group.cue.required } : {}
|
|
577
|
-
}
|
|
578
|
-
} : {}
|
|
579
|
-
}
|
|
580
|
-
} : {},
|
|
581
|
-
...mediaBudgets !== void 0 ? { mediaBudgets } : {}
|
|
582
|
-
};
|
|
574
|
+
if (!Number.isFinite(points) || points < 0) throw new Error(`planAttempt: slot "${slot.slotId}" resolved to ${String(points)} points. Points must be a finite, non-negative number, or the paper has no defensible total.`);
|
|
575
|
+
const mediaBudgets = mediaBudgetsOf(slot);
|
|
576
|
+
return {
|
|
577
|
+
slotId: slot.slotId,
|
|
578
|
+
index: slot.index,
|
|
579
|
+
activityId: slot.activity.id,
|
|
580
|
+
activityType: slot.activity.type,
|
|
581
|
+
points,
|
|
582
|
+
contentHash: contentHash(contentOf(slot.activity)),
|
|
583
|
+
...slot.group !== void 0 ? { group: {
|
|
584
|
+
id: slot.group.id,
|
|
585
|
+
...slot.group.title !== void 0 ? { title: slot.group.title } : {},
|
|
586
|
+
stimulusHash: contentHash(slot.group.stimulus),
|
|
587
|
+
...slot.group.cue !== void 0 ? { cue: {
|
|
588
|
+
id: slot.group.cue.id,
|
|
589
|
+
at: slot.group.cue.at,
|
|
590
|
+
...slot.group.cue.required !== void 0 ? { required: slot.group.cue.required } : {}
|
|
591
|
+
} } : {}
|
|
592
|
+
} } : {},
|
|
593
|
+
...mediaBudgets !== void 0 ? { mediaBudgets } : {}
|
|
594
|
+
};
|
|
583
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Freezes what an attempt is being served: the presented order, each slot's
|
|
598
|
+
* identity and worth, and a fingerprint of the content behind it.
|
|
599
|
+
*
|
|
600
|
+
* Call this once, when the attempt starts, and store the result beside the
|
|
601
|
+
* responses. Everything that decides the grade is then a historical fact
|
|
602
|
+
* rather than a re-read of content that may since have changed — which is what
|
|
603
|
+
* makes a re-grade, a remark or an appeal answerable.
|
|
604
|
+
*
|
|
605
|
+
* Ordering comes from `flattenSequence`, so a plan and a live render of the
|
|
606
|
+
* same entries with the same seed agree slot for slot.
|
|
607
|
+
*
|
|
608
|
+
* @throws Error when a shuffle is requested without a seed, when a group is
|
|
609
|
+
* empty, when two entries collide on a `slotKey`, or when `points`
|
|
610
|
+
* returns a value that is not finite and non-negative.
|
|
611
|
+
*/
|
|
584
612
|
function planAttempt(entries, options = {}) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
planHash: contentHash(
|
|
610
|
-
delivery === void 0 && scoring === void 0 ? planned : {
|
|
611
|
-
slots: planned,
|
|
612
|
-
...delivery !== void 0 ? { delivery } : {},
|
|
613
|
-
...scoring !== void 0 ? { scoring } : {}
|
|
614
|
-
}
|
|
615
|
-
),
|
|
616
|
-
...delivery !== void 0 ? { delivery } : {},
|
|
617
|
-
...scoring !== void 0 ? { scoring } : {},
|
|
618
|
-
slots: planned,
|
|
619
|
-
totalPoints
|
|
620
|
-
};
|
|
613
|
+
const { seed, shuffleEntries, points } = options;
|
|
614
|
+
const delivery = frozenDelivery(options.delivery);
|
|
615
|
+
const scoring = frozenScoring(options.scoring);
|
|
616
|
+
const slots = flattenSequence(entries, {
|
|
617
|
+
...shuffleEntries !== void 0 ? { shuffleEntries } : {},
|
|
618
|
+
...seed !== void 0 ? { seed } : {}
|
|
619
|
+
});
|
|
620
|
+
assertNoSplitBudgets(slots);
|
|
621
|
+
const planned = slots.map((slot) => planSlot(slot, points?.(slot) ?? 1));
|
|
622
|
+
const totalPoints = planned.reduce((sum, slot) => sum + slot.points, 0);
|
|
623
|
+
return {
|
|
624
|
+
planVersion: "1.0",
|
|
625
|
+
...seed !== void 0 ? { seed } : {},
|
|
626
|
+
...shuffleEntries !== void 0 ? { shuffleEntries } : {},
|
|
627
|
+
planHash: contentHash(delivery === void 0 && scoring === void 0 ? planned : {
|
|
628
|
+
slots: planned,
|
|
629
|
+
...delivery !== void 0 ? { delivery } : {},
|
|
630
|
+
...scoring !== void 0 ? { scoring } : {}
|
|
631
|
+
}),
|
|
632
|
+
...delivery !== void 0 ? { delivery } : {},
|
|
633
|
+
...scoring !== void 0 ? { scoring } : {},
|
|
634
|
+
slots: planned,
|
|
635
|
+
totalPoints
|
|
636
|
+
};
|
|
621
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Compares a stored plan with the content as it stands now, and reports every
|
|
640
|
+
* way they have drifted apart.
|
|
641
|
+
*
|
|
642
|
+
* This is the question a remark or an appeal actually asks: *is the paper I am
|
|
643
|
+
* looking at the paper this learner sat?* Ids alone cannot answer it, because
|
|
644
|
+
* they survive an edit unchanged.
|
|
645
|
+
*
|
|
646
|
+
* Rebuild the comparison plan with the SAME options the stored one recorded —
|
|
647
|
+
* its `seed`, its `shuffleEntries`, and the same `points` function — or the
|
|
648
|
+
* differences you see will be your own:
|
|
649
|
+
*
|
|
650
|
+
* ```ts
|
|
651
|
+
* const now = planAttempt(currentEntries, {
|
|
652
|
+
* seed: stored.seed,
|
|
653
|
+
* shuffleEntries: stored.shuffleEntries,
|
|
654
|
+
* points: pointsFor, // omit it and every slot reweights to 1
|
|
655
|
+
* });
|
|
656
|
+
* const drift = verifyAttemptPlan(stored, now);
|
|
657
|
+
* ```
|
|
658
|
+
*
|
|
659
|
+
* A drift is not automatically a problem — a fixed typo changes a hash without
|
|
660
|
+
* changing what was asked. It is a fact somebody has to be able to see.
|
|
661
|
+
*/
|
|
622
662
|
function verifyAttemptPlan(plan, current) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return {
|
|
656
|
-
matches: missingSlotIds.length === 0 && addedSlotIds.length === 0 && changedSlotIds.length === 0 && changedStimulusSlotIds.length === 0 && changedCueSlotIds.length === 0 && changedPointsSlotIds.length === 0 && reorderedSlotIds.length === 0 && !deliveryChanged && !scoringChanged,
|
|
657
|
-
missingSlotIds,
|
|
658
|
-
addedSlotIds,
|
|
659
|
-
changedSlotIds,
|
|
660
|
-
changedStimulusSlotIds,
|
|
661
|
-
changedCueSlotIds,
|
|
662
|
-
changedPointsSlotIds,
|
|
663
|
-
reorderedSlotIds,
|
|
664
|
-
// Present only when it is true, as `rejectedSlotIds` is on a composed
|
|
665
|
-
// score: a drift report between two plans without a policy — every one
|
|
666
|
-
// written before policies existed — keeps exactly the shape it had.
|
|
667
|
-
...deliveryChanged ? { deliveryChanged: true } : {},
|
|
668
|
-
...scoringChanged ? { scoringChanged: true } : {}
|
|
669
|
-
};
|
|
663
|
+
const before = new Map(plan.slots.map((slot) => [slot.slotId, slot]));
|
|
664
|
+
const after = new Map(current.slots.map((slot) => [slot.slotId, slot]));
|
|
665
|
+
const missingSlotIds = plan.slots.filter((s) => !after.has(s.slotId)).map((s) => s.slotId);
|
|
666
|
+
const addedSlotIds = current.slots.filter((s) => !before.has(s.slotId)).map((s) => s.slotId);
|
|
667
|
+
const changedSlotIds = [];
|
|
668
|
+
const changedStimulusSlotIds = [];
|
|
669
|
+
const changedCueSlotIds = [];
|
|
670
|
+
const changedPointsSlotIds = [];
|
|
671
|
+
const reorderedSlotIds = [];
|
|
672
|
+
const deliveryChanged = contentHash(plan.delivery ?? null) !== contentHash(current.delivery ?? null);
|
|
673
|
+
const scoringChanged = contentHash(plan.scoring ?? null) !== contentHash(current.scoring ?? null);
|
|
674
|
+
for (const slot of plan.slots) {
|
|
675
|
+
const now = after.get(slot.slotId);
|
|
676
|
+
if (now === void 0) continue;
|
|
677
|
+
if (now.contentHash !== slot.contentHash) changedSlotIds.push(slot.slotId);
|
|
678
|
+
if (now.group?.stimulusHash !== slot.group?.stimulusHash) changedStimulusSlotIds.push(slot.slotId);
|
|
679
|
+
if (contentHash(now.group?.cue ?? null) !== contentHash(slot.group?.cue ?? null)) changedCueSlotIds.push(slot.slotId);
|
|
680
|
+
if (now.points !== slot.points) changedPointsSlotIds.push(slot.slotId);
|
|
681
|
+
if (now.index !== slot.index) reorderedSlotIds.push(slot.slotId);
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
matches: missingSlotIds.length === 0 && addedSlotIds.length === 0 && changedSlotIds.length === 0 && changedStimulusSlotIds.length === 0 && changedCueSlotIds.length === 0 && changedPointsSlotIds.length === 0 && reorderedSlotIds.length === 0 && !deliveryChanged && !scoringChanged,
|
|
685
|
+
missingSlotIds,
|
|
686
|
+
addedSlotIds,
|
|
687
|
+
changedSlotIds,
|
|
688
|
+
changedStimulusSlotIds,
|
|
689
|
+
changedCueSlotIds,
|
|
690
|
+
changedPointsSlotIds,
|
|
691
|
+
reorderedSlotIds,
|
|
692
|
+
...deliveryChanged ? { deliveryChanged: true } : {},
|
|
693
|
+
...scoringChanged ? { scoringChanged: true } : {}
|
|
694
|
+
};
|
|
670
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* The policy a plan records: checked, then spelled out. `undefined` when there
|
|
698
|
+
* is none, so the plan carries no field at all rather than an empty one.
|
|
699
|
+
*/
|
|
671
700
|
function frozenDelivery(policy) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
`planAttempt: the delivery policy is not one to record \u2014 ${_nullishCoalesce(_optionalChain([first, 'optionalAccess', _23 => _23.message]), () => ( "invalid"))}` + (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : "")
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
return resolveDeliveryPolicy(checked.data);
|
|
701
|
+
if (policy === void 0 || policy === null) return;
|
|
702
|
+
const checked = validateDeliveryPolicy(policy);
|
|
703
|
+
if (!checked.success) {
|
|
704
|
+
const first = checked.issues[0];
|
|
705
|
+
throw new Error(`planAttempt: the delivery policy is not one to record — ${first?.message ?? "invalid"}` + (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ""));
|
|
706
|
+
}
|
|
707
|
+
return resolveDeliveryPolicy(checked.data);
|
|
683
708
|
}
|
|
709
|
+
/** The scoring policy a plan records, as {@link frozenDelivery} records a delivery policy. */
|
|
684
710
|
function frozenScoring(policy) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
`planAttempt: the scoring policy is not one to record \u2014 ${_nullishCoalesce(_optionalChain([first, 'optionalAccess', _24 => _24.message]), () => ( "invalid"))}` + (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : "")
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
return _chunkVSH7ECQLcjs.resolveItemScoringPolicy.call(void 0, checked.data);
|
|
711
|
+
if (policy === void 0 || policy === null) return;
|
|
712
|
+
const checked = require_scoring.validateItemScoringPolicy(policy);
|
|
713
|
+
if (!checked.success) {
|
|
714
|
+
const first = checked.issues[0];
|
|
715
|
+
throw new Error(`planAttempt: the scoring policy is not one to record — ${first?.message ?? "invalid"}` + (checked.issues.length > 1 ? ` (and ${checked.issues.length - 1} more)` : ""));
|
|
716
|
+
}
|
|
717
|
+
return require_scoring.resolveItemScoringPolicy(checked.data);
|
|
696
718
|
}
|
|
697
719
|
function missingOutcome(slot, policy) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
720
|
+
if (typeof policy === "function") return policy(slot);
|
|
721
|
+
if (policy === "zero") return {
|
|
722
|
+
status: "scored",
|
|
723
|
+
score: 0,
|
|
724
|
+
maxScore: 1,
|
|
725
|
+
passed: false,
|
|
726
|
+
feedback: null,
|
|
727
|
+
details: []
|
|
728
|
+
};
|
|
729
|
+
return {
|
|
730
|
+
status: "deferred",
|
|
731
|
+
reason: "no_response_recorded",
|
|
732
|
+
maxScore: 1
|
|
733
|
+
};
|
|
705
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Turns a plan plus whatever outcomes exist into the `ScoredItem[]`
|
|
737
|
+
* `composeAssessmentScore` consumes.
|
|
738
|
+
*
|
|
739
|
+
* The plan is the source of the denominator, not the outcomes. A slot the
|
|
740
|
+
* learner never reached still has to appear — otherwise it silently leaves the
|
|
741
|
+
* denominator and the remaining questions quietly become worth more than the
|
|
742
|
+
* paper says.
|
|
743
|
+
*
|
|
744
|
+
* **A missing outcome defaults to `deferred`, never `unscorable`.** The
|
|
745
|
+
* distinction decides a grade: `unscorable` means "a grade is never coming",
|
|
746
|
+
* so `composeAssessmentScore` drops the slot from the denominator AND lets the
|
|
747
|
+
* result go `final` — which turned a three-question paper with one answer into
|
|
748
|
+
* a final, passing 100%. `deferred` means "not yet", which holds the result
|
|
749
|
+
* `provisional` so nothing can be recorded. Pass `'zero'` once you know the
|
|
750
|
+
* attempt was submitted and the blanks are genuinely blanks.
|
|
751
|
+
*
|
|
752
|
+
* Pass outcomes keyed by `slotId`. Keys the plan does not know are ignored:
|
|
753
|
+
* a plan is the authority on what the attempt contained.
|
|
754
|
+
*/
|
|
706
755
|
function scoredItemsFromPlan(plan, outcomes, options = {}) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// resolve through the prototype chain and hand a FUNCTION to the scorer
|
|
715
|
-
// in place of an outcome.
|
|
716
|
-
outcome: Object.hasOwn(outcomes, slot.slotId) ? outcomes[slot.slotId] : missingOutcome(slot, policy)
|
|
717
|
-
}));
|
|
756
|
+
const policy = options.missing ?? "deferred";
|
|
757
|
+
return plan.slots.map((slot) => ({
|
|
758
|
+
slotId: slot.slotId,
|
|
759
|
+
activityId: slot.activityId,
|
|
760
|
+
points: slot.points,
|
|
761
|
+
outcome: Object.hasOwn(outcomes, slot.slotId) ? outcomes[slot.slotId] : missingOutcome(slot, policy)
|
|
762
|
+
}));
|
|
718
763
|
}
|
|
719
|
-
|
|
720
|
-
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/attempt-state.ts
|
|
766
|
+
/**
|
|
767
|
+
* A DEEP copy of the responses.
|
|
768
|
+
*
|
|
769
|
+
* A one-level spread is not enough: every `LearnerResponse` shape is itself an
|
|
770
|
+
* object (`selectedOptionIds`, `answers`, `text`), so a spread hands back a new
|
|
771
|
+
* map of the caller's SAME response objects. A consumer whose reducer updates
|
|
772
|
+
* an answer in place — an Immer draft, a push onto a multi-select, or
|
|
773
|
+
* `answers[blankId] = text`, which is the natural update for that shape — then
|
|
774
|
+
* mutates every snapshot ever taken. The stored snapshot retroactively becomes
|
|
775
|
+
* the new answer, `diffResponses` sees no change, and a delta autosave writes
|
|
776
|
+
* nothing for an edit the learner really made.
|
|
777
|
+
*/
|
|
721
778
|
function cloneResponses(responses) {
|
|
722
|
-
|
|
779
|
+
return typeof structuredClone === "function" ? structuredClone(responses) : JSON.parse(JSON.stringify(responses));
|
|
723
780
|
}
|
|
781
|
+
/** Slot ids present in the object but unknown to the plan. */
|
|
724
782
|
function unknownKeys(plan, keys) {
|
|
725
|
-
|
|
726
|
-
|
|
783
|
+
const known = new Set(plan.slots.map((slot) => slot.slotId));
|
|
784
|
+
return keys.filter((key) => !known.has(key));
|
|
727
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Captures an in-progress attempt as a storable snapshot, bound to its plan.
|
|
788
|
+
*
|
|
789
|
+
* Validated against the plan on the way in, not on the way out: a response
|
|
790
|
+
* stored under a slot the paper does not contain is a bug at the moment it is
|
|
791
|
+
* written, and finding it months later — when a learner tries to resume — is
|
|
792
|
+
* finding it far too late.
|
|
793
|
+
*
|
|
794
|
+
* The SDK reads no clock: pass `savedAt` if you want the snapshot stamped, so
|
|
795
|
+
* the function stays pure and its output stays reproducible in a test.
|
|
796
|
+
*
|
|
797
|
+
* @throws Error when a response or submitted slot is not in the plan, or when
|
|
798
|
+
* `index` is not a position the plan actually has.
|
|
799
|
+
*/
|
|
728
800
|
function serializeAttemptState(plan, progress) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
throw new Error(
|
|
747
|
-
`serializeAttemptState: index ${String(index)} is not a position in a ${plan.slots.length}-slot plan.`
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
return {
|
|
751
|
-
stateVersion: "1.0",
|
|
752
|
-
planHash: plan.planHash,
|
|
753
|
-
// Copied DEEPLY, not aliased: a snapshot that keeps mutating with the
|
|
754
|
-
// live attempt is not a snapshot. See cloneResponses.
|
|
755
|
-
responses: cloneResponses(progress.responses),
|
|
756
|
-
submittedSlotIds,
|
|
757
|
-
index,
|
|
758
|
-
...progress.savedAt !== void 0 ? { savedAt: progress.savedAt } : {}
|
|
759
|
-
};
|
|
801
|
+
const responseKeys = Object.keys(progress.responses);
|
|
802
|
+
const submittedSlotIds = [...progress.submittedSlotIds ?? []];
|
|
803
|
+
const strayResponses = unknownKeys(plan, responseKeys);
|
|
804
|
+
if (strayResponses.length > 0) throw new Error(`serializeAttemptState: response recorded for slot(s) the plan does not contain: ${strayResponses.join(", ")}. A response that belongs to no question cannot be scored and would be lost silently.`);
|
|
805
|
+
const straySubmitted = unknownKeys(plan, submittedSlotIds);
|
|
806
|
+
if (straySubmitted.length > 0) throw new Error(`serializeAttemptState: submitted slot(s) the plan does not contain: ${straySubmitted.join(", ")}.`);
|
|
807
|
+
const index = progress.index ?? 0;
|
|
808
|
+
const lastIndex = Math.max(0, plan.slots.length - 1);
|
|
809
|
+
if (!Number.isInteger(index) || index < 0 || index > lastIndex) throw new Error(`serializeAttemptState: index ${String(index)} is not a position in a ${plan.slots.length}-slot plan.`);
|
|
810
|
+
return {
|
|
811
|
+
stateVersion: "1.0",
|
|
812
|
+
planHash: plan.planHash,
|
|
813
|
+
responses: cloneResponses(progress.responses),
|
|
814
|
+
submittedSlotIds,
|
|
815
|
+
index,
|
|
816
|
+
...progress.savedAt !== void 0 ? { savedAt: progress.savedAt } : {}
|
|
817
|
+
};
|
|
760
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Reopens a stored snapshot against a plan, refusing anything that does not
|
|
821
|
+
* belong to it.
|
|
822
|
+
*
|
|
823
|
+
* The `planHash` check is the point. Slot ids are short and stable by design,
|
|
824
|
+
* so a snapshot from a DIFFERENT paper — last term's midterm, a sibling
|
|
825
|
+
* version, a copy-pasted attempt row — will happily line its answers up
|
|
826
|
+
* against the wrong questions and look entirely plausible doing it. Comparing
|
|
827
|
+
* the paper's fingerprint is what makes that impossible rather than unlikely.
|
|
828
|
+
*
|
|
829
|
+
* @throws Error when the snapshot belongs to a different plan, or references
|
|
830
|
+
* slots the plan does not contain.
|
|
831
|
+
*/
|
|
761
832
|
function restoreAttemptState(plan, state) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
"restoreAttemptState: the snapshot carries no responses object (a NULL column, or a partially written row)."
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
if (state.planHash !== plan.planHash) {
|
|
776
|
-
throw new Error(
|
|
777
|
-
`restoreAttemptState: this snapshot belongs to a different paper (snapshot ${state.planHash}, plan ${plan.planHash}). Restoring it would attach the learner's answers to questions they never saw.`
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
return serializeAttemptState(plan, {
|
|
781
|
-
responses: state.responses,
|
|
782
|
-
submittedSlotIds: state.submittedSlotIds,
|
|
783
|
-
index: state.index,
|
|
784
|
-
...state.savedAt !== void 0 ? { savedAt: state.savedAt } : {}
|
|
785
|
-
});
|
|
833
|
+
if (state === null || typeof state !== "object") throw new Error("restoreAttemptState: the snapshot is not an object.");
|
|
834
|
+
if (state.stateVersion !== "1.0") throw new Error(`restoreAttemptState: unsupported stateVersion ${JSON.stringify(state.stateVersion)}. This build understands "1.0"; a newer snapshot must be migrated before it is restored.`);
|
|
835
|
+
if (state.responses === null || typeof state.responses !== "object") throw new Error("restoreAttemptState: the snapshot carries no responses object (a NULL column, or a partially written row).");
|
|
836
|
+
if (state.planHash !== plan.planHash) throw new Error(`restoreAttemptState: this snapshot belongs to a different paper (snapshot ${state.planHash}, plan ${plan.planHash}). Restoring it would attach the learner's answers to questions they never saw.`);
|
|
837
|
+
return serializeAttemptState(plan, {
|
|
838
|
+
responses: state.responses,
|
|
839
|
+
submittedSlotIds: state.submittedSlotIds,
|
|
840
|
+
index: state.index,
|
|
841
|
+
...state.savedAt !== void 0 ? { savedAt: state.savedAt } : {}
|
|
842
|
+
});
|
|
786
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Reports how two snapshots' responses differ, slot by slot.
|
|
846
|
+
*
|
|
847
|
+
* Useful for an autosave that should only write what moved, and for an audit
|
|
848
|
+
* trail that has to show what a learner changed between two saves — including
|
|
849
|
+
* an answer they cleared, which a naive comparison of the later snapshot
|
|
850
|
+
* alone cannot see.
|
|
851
|
+
*
|
|
852
|
+
* Entries come back sorted by `slotId`, so the output is stable regardless of
|
|
853
|
+
* the order the two objects happened to be written in.
|
|
854
|
+
*/
|
|
787
855
|
function diffResponses(before, after) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const id = context.newId();
|
|
821
|
-
if (typeof id !== "string" || id === "") {
|
|
822
|
-
throw new Error("createItemGroupDraft: newId() must return a non-empty string.");
|
|
823
|
-
}
|
|
824
|
-
if (issued.has(id)) {
|
|
825
|
-
throw new Error(
|
|
826
|
-
`createItemGroupDraft: newId() returned "${id}" twice. Ids within one draft must differ.`
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
issued.add(id);
|
|
830
|
-
return id;
|
|
831
|
-
};
|
|
832
|
-
return {
|
|
833
|
-
schemaVersion: "1.0",
|
|
834
|
-
type: "item-group",
|
|
835
|
-
id: newId(),
|
|
836
|
-
stimulus: { id: newId(), kind: "text", body: "" },
|
|
837
|
-
items: []
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
function createInteractiveVideoDraft(context) {
|
|
841
|
-
const draft = createItemGroupDraft(context);
|
|
842
|
-
return {
|
|
843
|
-
...draft,
|
|
844
|
-
stimulus: { id: draft.stimulus.id, kind: "video", media: { type: "video", url: "" } },
|
|
845
|
-
timeline: { cues: [] }
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
function validateItemGroupDraft(draft) {
|
|
849
|
-
return _chunkDSDXL3JOcjs.withValidationScope.call(void 0, () => validateItemGroupDraftInScope(draft));
|
|
850
|
-
}
|
|
851
|
-
function validateItemGroupDraftInScope(draft) {
|
|
852
|
-
const issues = [];
|
|
853
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, draft)) {
|
|
854
|
-
issues.push(
|
|
855
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_not_an_object", [], "A group must be an object with a stimulus and its items.")
|
|
856
|
-
);
|
|
857
|
-
return { status: "invalid", issues };
|
|
858
|
-
}
|
|
859
|
-
issues.push(...checkGroupIdentity(draft));
|
|
860
|
-
if (_chunkDSDXL3JOcjs.isRecord.call(void 0, draft.stimulus)) {
|
|
861
|
-
issues.push(...checkStimulus(draft.stimulus));
|
|
862
|
-
} else if (_chunkDSDXL3JOcjs.isUnset.call(void 0, draft.stimulus)) {
|
|
863
|
-
issues.push(
|
|
864
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
865
|
-
"ig_stimulus_required",
|
|
866
|
-
["stimulus"],
|
|
867
|
-
"Add the passage, recording or image the questions are about."
|
|
868
|
-
)
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
issues.push(...checkItems(draft));
|
|
872
|
-
if (draft.timeline !== void 0 && draft.timeline !== null) {
|
|
873
|
-
issues.push(...checkTimeline(draft));
|
|
874
|
-
}
|
|
875
|
-
const captions = _chunkDSDXL3JOcjs.groupCaptionsField.call(void 0, draft, (captionsUrl) => !_chunkDSDXL3JOcjs.isUnwritten.call(void 0, captionsUrl));
|
|
876
|
-
if (captions !== void 0) {
|
|
877
|
-
issues.push(
|
|
878
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
879
|
-
"dc_captions_not_allowed",
|
|
880
|
-
["stimulus", "media", captions],
|
|
881
|
-
_chunkDSDXL3JOcjs.GROUP_CAPTIONS_REVEAL_DICTATION
|
|
882
|
-
)
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
const parsed = _chunk2JT2WUQ2cjs.ItemGroupSchema.safeParse(draft, { reportInput: true });
|
|
886
|
-
if (!parsed.success) {
|
|
887
|
-
for (const schemaIssue of parsed.error.issues) {
|
|
888
|
-
const path = schemaIssue.path.map(String);
|
|
889
|
-
if (issues.some((known) => _chunkDSDXL3JOcjs.covers.call(void 0, known.path, path))) {
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
if (path.length > 0 && _chunkDSDXL3JOcjs.refusesEmpty.call(void 0, draft, schemaIssue)) {
|
|
893
|
-
issues.push(
|
|
894
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
895
|
-
"null_not_allowed",
|
|
896
|
-
path,
|
|
897
|
-
"This field cannot be null. Leave it out if it is optional, or give it a value."
|
|
898
|
-
)
|
|
899
|
-
);
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
issues.push({
|
|
903
|
-
path,
|
|
904
|
-
message: schemaIssue.message,
|
|
905
|
-
code: schemaIssue.code,
|
|
906
|
-
severity: "invalid"
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
const stored = _chunk2JT2WUQ2cjs.validateItemGroup.call(void 0, draft);
|
|
911
|
-
if (issues.length === 0 && stored.success) {
|
|
912
|
-
return { status: "complete", data: stored.data, issues };
|
|
913
|
-
}
|
|
914
|
-
if (issues.length === 0) {
|
|
915
|
-
for (const error of stored.success ? [] : stored.errors) {
|
|
916
|
-
issues.push({ ...error, severity: "invalid" });
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
const invalid = issues.some((found) => found.severity !== "incomplete");
|
|
920
|
-
return { status: invalid ? "invalid" : "incomplete", issues };
|
|
921
|
-
}
|
|
922
|
-
function checkTimeline(draft) {
|
|
923
|
-
const issues = [];
|
|
924
|
-
const timeline = draft.timeline;
|
|
925
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, timeline)) {
|
|
926
|
-
return issues;
|
|
927
|
-
}
|
|
928
|
-
const stimulus = _chunkDSDXL3JOcjs.isRecord.call(void 0, draft.stimulus) ? draft.stimulus : void 0;
|
|
929
|
-
if (stimulus !== void 0 && !_chunkDSDXL3JOcjs.isUnwritten.call(void 0, stimulus.kind) && stimulus.kind !== "video") {
|
|
930
|
-
issues.push(
|
|
931
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
932
|
-
"ig_timeline_stimulus_kind",
|
|
933
|
-
["stimulus", "kind"],
|
|
934
|
-
"An interactive video needs a video: quizzes open at moments of it."
|
|
935
|
-
)
|
|
936
|
-
);
|
|
937
|
-
} else if (stimulus !== void 0 && _chunkDSDXL3JOcjs.isRecord.call(void 0, stimulus.media) && !_chunkDSDXL3JOcjs.isUnwritten.call(void 0, stimulus.media.type) && stimulus.media.type !== "video") {
|
|
938
|
-
issues.push(
|
|
939
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
940
|
-
"ig_timeline_stimulus_kind",
|
|
941
|
-
["stimulus", "media", "type"],
|
|
942
|
-
"Upload a video file. An embedded player from another site cannot be paused when a quiz opens."
|
|
943
|
-
)
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
if (draft.shuffle === "within-group") {
|
|
947
|
-
issues.push(
|
|
948
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
949
|
-
"ig_timeline_shuffle",
|
|
950
|
-
["shuffle"],
|
|
951
|
-
"The questions of an interactive video appear in the order their quizzes open, so they cannot be shuffled."
|
|
952
|
-
)
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
956
|
-
const itemIndexById = /* @__PURE__ */ new Map();
|
|
957
|
-
items.forEach((item, index) => {
|
|
958
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, item)) {
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
if (typeof item.id === "string" && item.id !== "") {
|
|
962
|
-
itemIndexById.set(item.id, index);
|
|
963
|
-
}
|
|
964
|
-
const type = item.type;
|
|
965
|
-
if (typeof type === "string" && type !== "" && type !== "item-group" && !_chunk2JT2WUQ2cjs.isInteractiveVideoItemType.call(void 0, type) && _chunkDSDXL3JOcjs.getActivityTypeDescriptor.call(void 0, type) !== void 0) {
|
|
966
|
-
issues.push(
|
|
967
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
968
|
-
"ig_timeline_item_type",
|
|
969
|
-
["items", index, "type"],
|
|
970
|
-
`An interactive video can hold ${_chunk2JT2WUQ2cjs.INTERACTIVE_VIDEO_ITEM_TYPES.join(", ")} questions only.`
|
|
971
|
-
)
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
if (type === "dictation" && _chunkDSDXL3JOcjs.isUnset.call(void 0, item.media)) {
|
|
975
|
-
issues.push(
|
|
976
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
977
|
-
"ig_timeline_dictation_media",
|
|
978
|
-
["items", index, "media"],
|
|
979
|
-
"Add the recording this dictation plays: inside a video it cannot play the video."
|
|
980
|
-
)
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
});
|
|
984
|
-
const cues = Array.isArray(timeline.cues) ? timeline.cues : [];
|
|
985
|
-
const quizIds = /* @__PURE__ */ new Set();
|
|
986
|
-
const placed = /* @__PURE__ */ new Set();
|
|
987
|
-
cues.forEach((cue, cueIndex) => {
|
|
988
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, cue)) {
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
const at = (field, ...rest) => [
|
|
992
|
-
"timeline",
|
|
993
|
-
"cues",
|
|
994
|
-
cueIndex,
|
|
995
|
-
field,
|
|
996
|
-
...rest
|
|
997
|
-
];
|
|
998
|
-
if (_chunkDSDXL3JOcjs.isMissingId.call(void 0, cue.id)) {
|
|
999
|
-
issues.push(_chunkDSDXL3JOcjs.issue.call(void 0, "ig_timeline_quiz_id_required", at("id"), "This quiz has no id."));
|
|
1000
|
-
} else if (typeof cue.id === "string") {
|
|
1001
|
-
if (quizIds.has(cue.id)) {
|
|
1002
|
-
issues.push(
|
|
1003
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_timeline_quiz_id_duplicate", at("id"), `Two quizzes share the id "${cue.id}".`)
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
quizIds.add(cue.id);
|
|
1007
|
-
}
|
|
1008
|
-
if (_chunkDSDXL3JOcjs.isUnset.call(void 0, cue.at)) {
|
|
1009
|
-
issues.push(
|
|
1010
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1011
|
-
"ig_timeline_quiz_time_required",
|
|
1012
|
-
at("at"),
|
|
1013
|
-
"Choose when in the video this quiz opens."
|
|
1014
|
-
)
|
|
1015
|
-
);
|
|
1016
|
-
} else if (typeof cue.at !== "number" || !Number.isFinite(cue.at) || cue.at < 0) {
|
|
1017
|
-
issues.push(
|
|
1018
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1019
|
-
"ig_timeline_quiz_time_invalid",
|
|
1020
|
-
at("at"),
|
|
1021
|
-
"A quiz opens at a number of seconds, 0 or more."
|
|
1022
|
-
)
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
if (_chunkDSDXL3JOcjs.isUnset.call(void 0, cue.itemIds) || Array.isArray(cue.itemIds) && cue.itemIds.length === 0) {
|
|
1026
|
-
issues.push(
|
|
1027
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_timeline_quiz_empty", at("itemIds"), "Add at least one question to this quiz.")
|
|
1028
|
-
);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (!Array.isArray(cue.itemIds)) {
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
cue.itemIds.forEach((itemId, position) => {
|
|
1035
|
-
if (typeof itemId !== "string") {
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
if (!itemIndexById.has(itemId)) {
|
|
1039
|
-
issues.push(
|
|
1040
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1041
|
-
"ig_timeline_quiz_unknown_item",
|
|
1042
|
-
at("itemIds", position),
|
|
1043
|
-
`This quiz names a question that is not in the video ("${itemId}").`
|
|
1044
|
-
)
|
|
1045
|
-
);
|
|
1046
|
-
} else if (placed.has(itemId)) {
|
|
1047
|
-
issues.push(
|
|
1048
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1049
|
-
"ig_timeline_item_duplicate",
|
|
1050
|
-
at("itemIds", position),
|
|
1051
|
-
"This question is already in a quiz: each question belongs to one quiz."
|
|
1052
|
-
)
|
|
1053
|
-
);
|
|
1054
|
-
}
|
|
1055
|
-
placed.add(itemId);
|
|
1056
|
-
});
|
|
1057
|
-
});
|
|
1058
|
-
for (const [itemId, index] of itemIndexById) {
|
|
1059
|
-
if (!placed.has(itemId)) {
|
|
1060
|
-
issues.push(
|
|
1061
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1062
|
-
"ig_timeline_item_unplaced",
|
|
1063
|
-
["items", index],
|
|
1064
|
-
"Add this question to a quiz, or the video never shows it."
|
|
1065
|
-
)
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
|
|
1070
|
-
let previousAt;
|
|
1071
|
-
chapters.forEach((chapter, index) => {
|
|
1072
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, chapter)) {
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
if (_chunkDSDXL3JOcjs.isUnwritten.call(void 0, chapter.title)) {
|
|
1076
|
-
issues.push(
|
|
1077
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1078
|
-
"ig_timeline_chapter_title",
|
|
1079
|
-
["timeline", "chapters", index, "title"],
|
|
1080
|
-
"Give this chapter a title."
|
|
1081
|
-
)
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
if (typeof chapter.at === "number" && Number.isFinite(chapter.at)) {
|
|
1085
|
-
if (previousAt !== void 0 && chapter.at <= previousAt) {
|
|
1086
|
-
issues.push(
|
|
1087
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1088
|
-
"ig_timeline_chapter_order",
|
|
1089
|
-
["timeline", "chapters", index, "at"],
|
|
1090
|
-
"Chapters must start in order, each after the one before it."
|
|
1091
|
-
)
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
previousAt = chapter.at;
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
return issues;
|
|
1098
|
-
}
|
|
1099
|
-
function checkGroupIdentity(draft) {
|
|
1100
|
-
const issues = [];
|
|
1101
|
-
if (draft.schemaVersion !== "1.0") {
|
|
1102
|
-
issues.push(
|
|
1103
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1104
|
-
"schema_version_invalid",
|
|
1105
|
-
["schemaVersion"],
|
|
1106
|
-
'The draft must have schemaVersion "1.0". A draft from createItemGroupDraft has it.'
|
|
1107
|
-
)
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
if (draft.type !== "item-group") {
|
|
1111
|
-
issues.push(_chunkDSDXL3JOcjs.issue.call(void 0, "type_mismatch", ["type"], `The draft's type must be "item-group".`));
|
|
1112
|
-
}
|
|
1113
|
-
if (_chunkDSDXL3JOcjs.isMissingId.call(void 0, draft.id)) {
|
|
1114
|
-
issues.push(_chunkDSDXL3JOcjs.issue.call(void 0, "id_required", ["id"], "The group has no id."));
|
|
1115
|
-
}
|
|
1116
|
-
if (!_chunkDSDXL3JOcjs.isUnset.call(void 0, draft.shuffle) && draft.shuffle !== "none" && draft.shuffle !== "within-group") {
|
|
1117
|
-
issues.push(
|
|
1118
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1119
|
-
"ig_shuffle_invalid",
|
|
1120
|
-
["shuffle"],
|
|
1121
|
-
'Shuffling is either "none" or "within-group". Leave it out to keep the authored order.'
|
|
1122
|
-
)
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
return issues;
|
|
1126
|
-
}
|
|
1127
|
-
var KINDS = /* @__PURE__ */ new Set(["text", "audio", "video", "image", "mixed"]);
|
|
1128
|
-
var MEDIA_FOR_KIND = {
|
|
1129
|
-
audio: ["audio"],
|
|
1130
|
-
video: ["video", "embed"],
|
|
1131
|
-
image: ["image"],
|
|
1132
|
-
mixed: ["audio", "video", "image", "embed"]
|
|
1133
|
-
};
|
|
1134
|
-
function checkStimulus(stimulus) {
|
|
1135
|
-
const issues = [];
|
|
1136
|
-
const at = (...rest) => ["stimulus", ...rest];
|
|
1137
|
-
if (_chunkDSDXL3JOcjs.isMissingId.call(void 0, stimulus.id)) {
|
|
1138
|
-
issues.push(_chunkDSDXL3JOcjs.issue.call(void 0, "ig_stimulus_id_required", at("id"), "The stimulus has no id."));
|
|
1139
|
-
}
|
|
1140
|
-
const kind = stimulus.kind;
|
|
1141
|
-
if (_chunkDSDXL3JOcjs.isUnwritten.call(void 0, kind)) {
|
|
1142
|
-
issues.push(
|
|
1143
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1144
|
-
"ig_stimulus_kind_required",
|
|
1145
|
-
at("kind"),
|
|
1146
|
-
"Choose what the questions are about: a text, a recording, a video, an image, or a mix."
|
|
1147
|
-
)
|
|
1148
|
-
);
|
|
1149
|
-
} else if (typeof kind !== "string" || !KINDS.has(kind)) {
|
|
1150
|
-
if (typeof kind === "string") {
|
|
1151
|
-
issues.push(
|
|
1152
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1153
|
-
"ig_stimulus_kind_invalid",
|
|
1154
|
-
at("kind"),
|
|
1155
|
-
`"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`
|
|
1156
|
-
)
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
if (_chunkDSDXL3JOcjs.isRecord.call(void 0, stimulus.media)) {
|
|
1160
|
-
issues.push(..._chunkDSDXL3JOcjs.checkMedia.call(void 0, stimulus.media, at("media")));
|
|
1161
|
-
}
|
|
1162
|
-
return issues;
|
|
1163
|
-
}
|
|
1164
|
-
const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
|
|
1165
|
-
const htmlPresent = !_chunkDSDXL3JOcjs.isUnset.call(void 0, stimulus.bodyHtml);
|
|
1166
|
-
if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) {
|
|
1167
|
-
issues.push(
|
|
1168
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1169
|
-
"ig_stimulus_body_required",
|
|
1170
|
-
at("body"),
|
|
1171
|
-
_chunkDSDXL3JOcjs.isUnwritten.call(void 0, stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."
|
|
1172
|
-
)
|
|
1173
|
-
);
|
|
1174
|
-
}
|
|
1175
|
-
if (kind !== "text" && _chunkDSDXL3JOcjs.isUnset.call(void 0, stimulus.media)) {
|
|
1176
|
-
issues.push(
|
|
1177
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_stimulus_media_required", at("media"), "Add the recording, video or image.")
|
|
1178
|
-
);
|
|
1179
|
-
} else if (_chunkDSDXL3JOcjs.isRecord.call(void 0, stimulus.media)) {
|
|
1180
|
-
const allowed = MEDIA_FOR_KIND[kind];
|
|
1181
|
-
const mediaType = stimulus.media.type;
|
|
1182
|
-
if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) {
|
|
1183
|
-
issues.push(
|
|
1184
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1185
|
-
"ig_stimulus_media_kind",
|
|
1186
|
-
at("media", "type"),
|
|
1187
|
-
`A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`
|
|
1188
|
-
)
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
issues.push(..._chunkDSDXL3JOcjs.checkMedia.call(void 0, stimulus.media, at("media")));
|
|
1192
|
-
}
|
|
1193
|
-
return issues;
|
|
1194
|
-
}
|
|
1195
|
-
function checkItems(draft) {
|
|
1196
|
-
const issues = [];
|
|
1197
|
-
const items = _chunkDSDXL3JOcjs.isUnset.call(void 0, draft.items) ? [] : draft.items;
|
|
1198
|
-
if (!Array.isArray(items)) {
|
|
1199
|
-
return issues;
|
|
1200
|
-
}
|
|
1201
|
-
if (items.length === 0) {
|
|
1202
|
-
issues.push(
|
|
1203
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_items_required", ["items"], "Add at least one question about this material.")
|
|
1204
|
-
);
|
|
1205
|
-
return issues;
|
|
1206
|
-
}
|
|
1207
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1208
|
-
const repeated = /* @__PURE__ */ new Set();
|
|
1209
|
-
for (const [index, item] of items.entries()) {
|
|
1210
|
-
if (!_chunkDSDXL3JOcjs.isRecord.call(void 0, item)) {
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
const ordinal = index + 1;
|
|
1214
|
-
if (_chunkDSDXL3JOcjs.isMissingId.call(void 0, item.id)) {
|
|
1215
|
-
issues.push(
|
|
1216
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_item_id_required", ["items", index, "id"], `Question ${ordinal} has no id.`)
|
|
1217
|
-
);
|
|
1218
|
-
} else if (typeof item.id === "string") {
|
|
1219
|
-
if (seen.has(item.id)) {
|
|
1220
|
-
repeated.add(item.id);
|
|
1221
|
-
}
|
|
1222
|
-
seen.add(item.id);
|
|
1223
|
-
}
|
|
1224
|
-
if (item.type === "item-group") {
|
|
1225
|
-
issues.push(
|
|
1226
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1227
|
-
"ig_item_nested_group",
|
|
1228
|
-
["items", index, "type"],
|
|
1229
|
-
"Groups do not nest: every item must be an activity."
|
|
1230
|
-
)
|
|
1231
|
-
);
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
if (_chunkDSDXL3JOcjs.isUnwritten.call(void 0, item.type)) {
|
|
1235
|
-
issues.push(
|
|
1236
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1237
|
-
"ig_item_type_required",
|
|
1238
|
-
["items", index, "type"],
|
|
1239
|
-
`Choose what kind of question ${ordinal} is.`
|
|
1240
|
-
)
|
|
1241
|
-
);
|
|
1242
|
-
continue;
|
|
1243
|
-
}
|
|
1244
|
-
if (typeof item.type !== "string") {
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
let result;
|
|
1248
|
-
try {
|
|
1249
|
-
result = validateDraft(item.type, item);
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
if (!(error instanceof _chunkDSDXL3JOcjs.UnknownActivityTypeError) || error.activityType !== item.type) {
|
|
1252
|
-
throw error;
|
|
1253
|
-
}
|
|
1254
|
-
issues.push(
|
|
1255
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1256
|
-
"ig_item_type_unknown",
|
|
1257
|
-
["items", index, "type"],
|
|
1258
|
-
`"${item.type}" is not a registered activity type.`
|
|
1259
|
-
)
|
|
1260
|
-
);
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1263
|
-
for (const found of result.issues) {
|
|
1264
|
-
issues.push({ ...found, path: ["items", String(index), ...found.path] });
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
for (const id of repeated) {
|
|
1268
|
-
issues.push(
|
|
1269
|
-
_chunkDSDXL3JOcjs.issue.call(void 0, "ig_item_id_duplicate", ["items"], `More than one question has the id "${id}".`)
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
return issues;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// src/authoring/index.ts
|
|
1276
|
-
function validateDraft(type, draft) {
|
|
1277
|
-
return _chunkDSDXL3JOcjs.withValidationScope.call(void 0, () => validateDraftInScope(type, draft));
|
|
856
|
+
const slotIds = [.../* @__PURE__ */ new Set([...Object.keys(before.responses), ...Object.keys(after.responses)])];
|
|
857
|
+
slotIds.sort();
|
|
858
|
+
const entries = [];
|
|
859
|
+
for (const slotId of slotIds) {
|
|
860
|
+
const had = Object.hasOwn(before.responses, slotId);
|
|
861
|
+
const has = Object.hasOwn(after.responses, slotId);
|
|
862
|
+
const from = before.responses[slotId];
|
|
863
|
+
const to = after.responses[slotId];
|
|
864
|
+
if (had && !has) {
|
|
865
|
+
entries.push({
|
|
866
|
+
slotId,
|
|
867
|
+
change: "removed",
|
|
868
|
+
...from !== void 0 ? { before: from } : {}
|
|
869
|
+
});
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (!had && has) {
|
|
873
|
+
entries.push({
|
|
874
|
+
slotId,
|
|
875
|
+
change: "added",
|
|
876
|
+
...to !== void 0 ? { after: to } : {}
|
|
877
|
+
});
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (canonicalJson(from) !== canonicalJson(to)) entries.push({
|
|
881
|
+
slotId,
|
|
882
|
+
change: "changed",
|
|
883
|
+
...from !== void 0 ? { before: from } : {},
|
|
884
|
+
...to !== void 0 ? { after: to } : {}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
return entries;
|
|
1278
888
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
throw new (0, _chunkDSDXL3JOcjs.UnknownActivityTypeError)(String(type));
|
|
1283
|
-
}
|
|
1284
|
-
const reported = _chunkDSDXL3JOcjs.isRecord.call(void 0, draft) && _optionalChain([descriptor, 'access', _25 => _25.authoring, 'optionalAccess', _26 => _26.checkDraft]) !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
|
|
1285
|
-
const issues = [...reported];
|
|
1286
|
-
const parsed = _chunkDSDXL3JOcjs.readSchema.call(void 0, descriptor.schema, draft, `Activity type "${descriptor.type}"`, {
|
|
1287
|
-
reportInput: true
|
|
1288
|
-
});
|
|
1289
|
-
if (!parsed.success) {
|
|
1290
|
-
const covered = _chunkDSDXL3JOcjs.coveredPathsOf.call(void 0, reported);
|
|
1291
|
-
const reportedEmpty = /* @__PURE__ */ new Set();
|
|
1292
|
-
for (const schemaIssue of parsed.issues) {
|
|
1293
|
-
const path = schemaIssue.path.map(String);
|
|
1294
|
-
if (covered.has(_chunkDSDXL3JOcjs.pathKey.call(void 0, path))) {
|
|
1295
|
-
continue;
|
|
1296
|
-
}
|
|
1297
|
-
if (path.length > 0 && _chunkDSDXL3JOcjs.refusesEmpty.call(void 0, draft, schemaIssue)) {
|
|
1298
|
-
const key = JSON.stringify(path);
|
|
1299
|
-
if (!reportedEmpty.has(key)) {
|
|
1300
|
-
reportedEmpty.add(key);
|
|
1301
|
-
issues.push(
|
|
1302
|
-
_chunkDSDXL3JOcjs.issue.call(void 0,
|
|
1303
|
-
"null_not_allowed",
|
|
1304
|
-
path,
|
|
1305
|
-
typeof schemaIssue.path.at(-1) === "number" ? "This entry has no value. Remove it, or fill it in." : "This field cannot be null. Leave it out if it is optional, or give it a value."
|
|
1306
|
-
)
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
issues.push({
|
|
1312
|
-
path,
|
|
1313
|
-
message: schemaIssue.message,
|
|
1314
|
-
code: schemaIssue.code,
|
|
1315
|
-
severity: "invalid"
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
if (parsed.success && issues.length === 0) {
|
|
1320
|
-
return { status: "complete", data: parsed.data, issues };
|
|
1321
|
-
}
|
|
1322
|
-
const invalid = issues.some((found) => found.severity !== "incomplete");
|
|
1323
|
-
return { status: invalid ? "invalid" : "incomplete", issues };
|
|
1324
|
-
}
|
|
1325
|
-
function createDraft(type, context) {
|
|
1326
|
-
const descriptor = _chunkDSDXL3JOcjs.getActivityTypeDescriptor.call(void 0, type);
|
|
1327
|
-
if (descriptor === void 0) {
|
|
1328
|
-
throw new (0, _chunkDSDXL3JOcjs.UnknownActivityTypeError)(String(type));
|
|
1329
|
-
}
|
|
1330
|
-
const create = _optionalChain([descriptor, 'access', _27 => _27.authoring, 'optionalAccess', _28 => _28.createDraft]);
|
|
1331
|
-
if (create === void 0) {
|
|
1332
|
-
throw new Error(
|
|
1333
|
-
`Activity type "${String(type)}" declares no authoring.createDraft, so there is no empty draft to create. Build the draft yourself; validateDraft works for every registered type.`
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
const issued = /* @__PURE__ */ new Set();
|
|
1337
|
-
const newId = () => {
|
|
1338
|
-
const id = context.newId();
|
|
1339
|
-
if (typeof id !== "string" || id === "") {
|
|
1340
|
-
throw new Error("createDraft: newId() must return a non-empty string.");
|
|
1341
|
-
}
|
|
1342
|
-
if (issued.has(id)) {
|
|
1343
|
-
throw new Error(
|
|
1344
|
-
`createDraft: newId() returned "${id}" twice. Ids within one draft must differ \u2014 two options sharing an id cannot be scored apart.`
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
issued.add(id);
|
|
1348
|
-
return id;
|
|
1349
|
-
};
|
|
1350
|
-
return create({ newId });
|
|
1351
|
-
}
|
|
1352
|
-
function normalise(found) {
|
|
1353
|
-
const documented = Object.hasOwn(_chunkDSDXL3JOcjs.DRAFT_ISSUE_SEVERITY, found.code) ? _chunkDSDXL3JOcjs.DRAFT_ISSUE_SEVERITY[found.code] : void 0;
|
|
1354
|
-
return {
|
|
1355
|
-
...found,
|
|
1356
|
-
path: found.path.map(String),
|
|
1357
|
-
severity: _nullishCoalesce(documented, () => ( (found.severity === "incomplete" ? "incomplete" : "invalid")))
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
// src/redact.ts
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/redact.ts
|
|
891
|
+
/** Merges per-call overrides over a registered policy, one level deep. */
|
|
1362
892
|
function mergePolicy(base, overrides) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
893
|
+
if (overrides === void 0) return base;
|
|
894
|
+
const merged = { ...base };
|
|
895
|
+
for (const [key, override] of Object.entries(overrides)) {
|
|
896
|
+
const current = merged[key];
|
|
897
|
+
merged[key] = typeof override === "object" && typeof current === "object" ? {
|
|
898
|
+
...current,
|
|
899
|
+
...override
|
|
900
|
+
} : override;
|
|
901
|
+
}
|
|
902
|
+
return merged;
|
|
1372
903
|
}
|
|
1373
904
|
function isSensitivity(value) {
|
|
1374
|
-
|
|
905
|
+
return typeof value === "string";
|
|
1375
906
|
}
|
|
1376
907
|
function keepField(sensitivity, reveal) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
return sensitivity === "answer-key" && reveal === "after-submit";
|
|
908
|
+
if (sensitivity === "public") return true;
|
|
909
|
+
return sensitivity === "answer-key" && reveal === "after-submit";
|
|
1381
910
|
}
|
|
911
|
+
/** An object projection that kept no fields at all. Arrays are never dropped. */
|
|
1382
912
|
function isEmptyObject(value) {
|
|
1383
|
-
|
|
913
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
1384
914
|
}
|
|
1385
915
|
function redactValue(value, policy, reveal) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
if (isSensitivity(classification)) {
|
|
1403
|
-
if (keepField(classification, reveal)) {
|
|
1404
|
-
output[key] = fieldValue;
|
|
1405
|
-
}
|
|
1406
|
-
continue;
|
|
1407
|
-
}
|
|
1408
|
-
const nested = redactValue(fieldValue, classification, reveal);
|
|
1409
|
-
if (nested !== void 0 && !isEmptyObject(nested)) {
|
|
1410
|
-
output[key] = nested;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return output;
|
|
916
|
+
if (Array.isArray(value)) return value.map((element) => redactValue(element, policy, reveal));
|
|
917
|
+
if (value === null || typeof value !== "object") return;
|
|
918
|
+
const source = value;
|
|
919
|
+
const output = {};
|
|
920
|
+
for (const [key, fieldValue] of Object.entries(source)) {
|
|
921
|
+
if (fieldValue === void 0) continue;
|
|
922
|
+
const classification = policy[key];
|
|
923
|
+
if (classification === void 0) continue;
|
|
924
|
+
if (isSensitivity(classification)) {
|
|
925
|
+
if (keepField(classification, reveal)) output[key] = fieldValue;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const nested = redactValue(fieldValue, classification, reveal);
|
|
929
|
+
if (nested !== void 0 && !isEmptyObject(nested)) output[key] = nested;
|
|
930
|
+
}
|
|
931
|
+
return output;
|
|
1414
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Produces the learner-safe projection of activity data (R7), driven by the
|
|
935
|
+
* `fieldPolicy` registered for `data.type`. Fail-closed and exhaustive by
|
|
936
|
+
* construction: a field the policy does not classify is removed — including
|
|
937
|
+
* unknown passthrough fields — so a NEW field added by a future SDK or a
|
|
938
|
+
* consumer sidecar can never leak through an out-of-date redactor.
|
|
939
|
+
*
|
|
940
|
+
* With the default `reveal: 'none'`, the output validates against the type's
|
|
941
|
+
* strict redacted schema (verified here; an invalid projection throws
|
|
942
|
+
* {@link ActivitySchemaError} rather than shipping an unproven payload).
|
|
943
|
+
*
|
|
944
|
+
* @throws UnknownActivityTypeError when `data.type` is not registered.
|
|
945
|
+
* @throws Error when the registered descriptor declares no `fieldPolicy`
|
|
946
|
+
* (redaction cannot guess sensitivities).
|
|
947
|
+
*/
|
|
1415
948
|
function redact(data, options = {}) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
`Activity type "${descriptor.type}"`
|
|
1434
|
-
);
|
|
1435
|
-
if (!parsed.success) {
|
|
1436
|
-
throw new (0, _chunkDSDXL3JOcjs.ActivitySchemaError)(
|
|
1437
|
-
descriptor.type,
|
|
1438
|
-
parsed.issues.map((issue2) => ({
|
|
1439
|
-
path: issue2.path.map(String),
|
|
1440
|
-
message: issue2.message,
|
|
1441
|
-
code: issue2.code
|
|
1442
|
-
}))
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
return result;
|
|
949
|
+
const reveal = options.reveal ?? "none";
|
|
950
|
+
const descriptor = require_registry.getActivityTypeDescriptor(data.type);
|
|
951
|
+
if (descriptor === void 0) throw new require_registry.UnknownActivityTypeError(String(data.type));
|
|
952
|
+
if (descriptor.fieldPolicy === void 0) throw new Error(`Activity type "${descriptor.type}" has no fieldPolicy; redact() cannot run fail-closed redaction without one.`);
|
|
953
|
+
const result = {
|
|
954
|
+
...redactValue(data, mergePolicy(descriptor.fieldPolicy, options.policy), reveal),
|
|
955
|
+
redacted: true
|
|
956
|
+
};
|
|
957
|
+
if (reveal === "none" && descriptor.redactedSchema !== void 0) {
|
|
958
|
+
const parsed = require_registry.readSchema(descriptor.redactedSchema, result, `Activity type "${descriptor.type}"`);
|
|
959
|
+
if (!parsed.success) throw new require_registry.ActivitySchemaError(descriptor.type, parsed.issues.map((issue) => ({
|
|
960
|
+
path: issue.path.map(String),
|
|
961
|
+
message: issue.message,
|
|
962
|
+
code: issue.code
|
|
963
|
+
})));
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
1447
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* Asserts that `data` is a learner-safe redacted projection: it carries the
|
|
969
|
+
* `redacted: true` marker and, when its type registers a strict redacted
|
|
970
|
+
* schema, validates against it (proving the absence of answer-key fields).
|
|
971
|
+
* Use this at the server boundary before sending activity data to a client
|
|
972
|
+
* that must not hold the key.
|
|
973
|
+
*/
|
|
1448
974
|
function assertRedacted(data) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
code: "custom"
|
|
1475
|
-
}
|
|
1476
|
-
]);
|
|
1477
|
-
}
|
|
1478
|
-
const parsed = _chunkDSDXL3JOcjs.readSchema.call(void 0,
|
|
1479
|
-
descriptor.redactedSchema,
|
|
1480
|
-
candidate,
|
|
1481
|
-
`Activity type "${descriptor.type}"`
|
|
1482
|
-
);
|
|
1483
|
-
if (!parsed.success) {
|
|
1484
|
-
throw new (0, _chunkDSDXL3JOcjs.ActivitySchemaError)(
|
|
1485
|
-
descriptor.type,
|
|
1486
|
-
parsed.issues.map((issue2) => ({
|
|
1487
|
-
path: issue2.path.map(String),
|
|
1488
|
-
message: issue2.message,
|
|
1489
|
-
code: issue2.code
|
|
1490
|
-
}))
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
975
|
+
if (typeof data !== "object" || data === null) throw new require_registry.ActivitySchemaError("unknown", [{
|
|
976
|
+
path: [],
|
|
977
|
+
message: "Redacted activity data must be an object.",
|
|
978
|
+
code: "invalid_type"
|
|
979
|
+
}]);
|
|
980
|
+
const candidate = data;
|
|
981
|
+
if (candidate.redacted !== true) throw new require_registry.ActivitySchemaError(String(candidate.type ?? "unknown"), [{
|
|
982
|
+
path: ["redacted"],
|
|
983
|
+
message: "Missing redacted marker — this payload is not a redact() projection.",
|
|
984
|
+
code: "custom"
|
|
985
|
+
}]);
|
|
986
|
+
const type = typeof candidate.type === "string" ? candidate.type : "";
|
|
987
|
+
const descriptor = require_registry.getActivityTypeDescriptor(type);
|
|
988
|
+
if (descriptor === void 0) throw new require_registry.UnknownActivityTypeError(type);
|
|
989
|
+
if (descriptor.redactedSchema === void 0) throw new require_registry.ActivitySchemaError(descriptor.type, [{
|
|
990
|
+
path: [],
|
|
991
|
+
message: `Activity type "${descriptor.type}" registers no redactedSchema; assertRedacted cannot prove this payload is learner-safe.`,
|
|
992
|
+
code: "custom"
|
|
993
|
+
}]);
|
|
994
|
+
const parsed = require_registry.readSchema(descriptor.redactedSchema, candidate, `Activity type "${descriptor.type}"`);
|
|
995
|
+
if (!parsed.success) throw new require_registry.ActivitySchemaError(descriptor.type, parsed.issues.map((issue) => ({
|
|
996
|
+
path: issue.path.map(String),
|
|
997
|
+
message: issue.message,
|
|
998
|
+
code: issue.code
|
|
999
|
+
})));
|
|
1493
1000
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
chapters: {
|
|
1528
|
-
at: "public",
|
|
1529
|
-
title: "public"
|
|
1530
|
-
},
|
|
1531
|
-
navigation: "public"
|
|
1532
|
-
}
|
|
1001
|
+
/** The group container's own fields. `items` is handled separately, per item type. */
|
|
1002
|
+
const ITEM_GROUP_FIELD_POLICY = {
|
|
1003
|
+
schemaVersion: "public",
|
|
1004
|
+
type: "public",
|
|
1005
|
+
id: "public",
|
|
1006
|
+
title: "public",
|
|
1007
|
+
slotKey: "public",
|
|
1008
|
+
shuffle: "public",
|
|
1009
|
+
stimulus: {
|
|
1010
|
+
id: "public",
|
|
1011
|
+
kind: "public",
|
|
1012
|
+
title: "public",
|
|
1013
|
+
body: "public",
|
|
1014
|
+
bodyHtml: "public",
|
|
1015
|
+
media: require_registry.MEDIA_FIELD_POLICY,
|
|
1016
|
+
locale: "public",
|
|
1017
|
+
attribution: "public",
|
|
1018
|
+
transcript: "author-only"
|
|
1019
|
+
},
|
|
1020
|
+
timeline: {
|
|
1021
|
+
cues: {
|
|
1022
|
+
id: "public",
|
|
1023
|
+
at: "public",
|
|
1024
|
+
itemIds: "public",
|
|
1025
|
+
title: "public",
|
|
1026
|
+
required: "public"
|
|
1027
|
+
},
|
|
1028
|
+
chapters: {
|
|
1029
|
+
at: "public",
|
|
1030
|
+
title: "public"
|
|
1031
|
+
},
|
|
1032
|
+
navigation: "public"
|
|
1033
|
+
}
|
|
1533
1034
|
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Produces the learner-safe projection of an item group: the container and
|
|
1037
|
+
* stimulus under their own fail-closed policy (the author transcript goes;
|
|
1038
|
+
* the passage, media and attribution stay — the learner is meant to see
|
|
1039
|
+
* them), and every item through {@link redact} with the same `options`, so
|
|
1040
|
+
* a per-call `policy` tightens each item exactly as it would alone.
|
|
1041
|
+
*
|
|
1042
|
+
* With the default `reveal: 'none'` the container is verified against the
|
|
1043
|
+
* strict redacted schema; each item was already verified by `redact`.
|
|
1044
|
+
*/
|
|
1534
1045
|
function redactItemGroup(group, options = {}) {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
code: issue2.code
|
|
1552
|
-
}))
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
return result;
|
|
1046
|
+
const reveal = options.reveal ?? "none";
|
|
1047
|
+
const { items, ...container } = group;
|
|
1048
|
+
const result = {
|
|
1049
|
+
...redactValue(container, ITEM_GROUP_FIELD_POLICY, reveal),
|
|
1050
|
+
items: items.map((item) => redact(item, options)),
|
|
1051
|
+
redacted: true
|
|
1052
|
+
};
|
|
1053
|
+
if (reveal === "none") {
|
|
1054
|
+
const parsed = require_item_group.RedactedItemGroupSchema.safeParse(result);
|
|
1055
|
+
if (!parsed.success) throw new require_registry.ActivitySchemaError("item-group", parsed.error.issues.map((issue) => ({
|
|
1056
|
+
path: issue.path.map(String),
|
|
1057
|
+
message: issue.message,
|
|
1058
|
+
code: issue.code
|
|
1059
|
+
})));
|
|
1060
|
+
}
|
|
1061
|
+
return result;
|
|
1557
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Asserts that `data` is a learner-safe item group: it carries the marker,
|
|
1065
|
+
* its container and stimulus satisfy the strict redacted schema (so no
|
|
1066
|
+
* transcript, no unclassified field), and EVERY item passes
|
|
1067
|
+
* {@link assertRedacted} against its own type's redacted schema. Item
|
|
1068
|
+
* failures are reported at `items.<index>.…`. Use it at the server boundary
|
|
1069
|
+
* before sending a group to a client that must not hold the key.
|
|
1070
|
+
*/
|
|
1558
1071
|
function assertRedactedItemGroup(data) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
"item-group",
|
|
1592
|
-
error.errors.map((issue2) => ({
|
|
1593
|
-
...issue2,
|
|
1594
|
-
path: ["items", String(index), ...issue2.path]
|
|
1595
|
-
}))
|
|
1596
|
-
);
|
|
1597
|
-
}
|
|
1598
|
-
throw error;
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1072
|
+
if (typeof data !== "object" || data === null) throw new require_registry.ActivitySchemaError("item-group", [{
|
|
1073
|
+
path: [],
|
|
1074
|
+
message: "Redacted item group must be an object.",
|
|
1075
|
+
code: "invalid_type"
|
|
1076
|
+
}]);
|
|
1077
|
+
const candidate = data;
|
|
1078
|
+
if (candidate.redacted !== true) throw new require_registry.ActivitySchemaError("item-group", [{
|
|
1079
|
+
path: ["redacted"],
|
|
1080
|
+
message: "Missing redacted marker — this payload is not a redactItemGroup() projection.",
|
|
1081
|
+
code: "custom"
|
|
1082
|
+
}]);
|
|
1083
|
+
const parsed = require_item_group.RedactedItemGroupSchema.safeParse(candidate);
|
|
1084
|
+
if (!parsed.success) throw new require_registry.ActivitySchemaError("item-group", parsed.error.issues.map((issue) => ({
|
|
1085
|
+
path: issue.path.map(String),
|
|
1086
|
+
message: issue.message,
|
|
1087
|
+
code: issue.code
|
|
1088
|
+
})));
|
|
1089
|
+
parsed.data.items.forEach((item, index) => {
|
|
1090
|
+
try {
|
|
1091
|
+
assertRedacted(item);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (error instanceof require_registry.ActivitySchemaError) throw new require_registry.ActivitySchemaError("item-group", error.errors.map((issue) => ({
|
|
1094
|
+
...issue,
|
|
1095
|
+
path: [
|
|
1096
|
+
"items",
|
|
1097
|
+
String(index),
|
|
1098
|
+
...issue.path
|
|
1099
|
+
]
|
|
1100
|
+
})));
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1601
1104
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/timeline.ts
|
|
1107
|
+
/**
|
|
1108
|
+
* Reads a stored {@link MediaProgress}. Never throws: anything that is not one
|
|
1109
|
+
* — a string, a stale shape, an object whose getters throw — answers `null`,
|
|
1110
|
+
* and the player starts from the beginning.
|
|
1111
|
+
*
|
|
1112
|
+
* What it keeps, it clamps. A stored position is host storage, which is input
|
|
1113
|
+
* from a stranger by the time it is read back: a negative `at`, a `furthest`
|
|
1114
|
+
* behind `at`, or a position past the end of a video that has since been
|
|
1115
|
+
* re-cut must not put the playhead somewhere it cannot be. Give `duration`
|
|
1116
|
+
* once the video's metadata is known, and both positions are held inside it.
|
|
1117
|
+
*/
|
|
1604
1118
|
function readMediaProgress(value, duration) {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
return {
|
|
1623
|
-
progressVersion: "1.0",
|
|
1624
|
-
at: clampedAt,
|
|
1625
|
-
furthest: Math.min(Math.max(clampedAt, reached), end)
|
|
1626
|
-
};
|
|
1119
|
+
let version;
|
|
1120
|
+
let at;
|
|
1121
|
+
let furthest;
|
|
1122
|
+
try {
|
|
1123
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1124
|
+
({progressVersion: version, at, furthest} = value);
|
|
1125
|
+
} catch {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
if (version !== "1.0" || typeof at !== "number" || !Number.isFinite(at)) return null;
|
|
1129
|
+
const end = typeof duration === "number" && Number.isFinite(duration) && duration >= 0 ? duration : Number.POSITIVE_INFINITY;
|
|
1130
|
+
const clampedAt = Math.min(Math.max(0, at), end);
|
|
1131
|
+
return {
|
|
1132
|
+
progressVersion: "1.0",
|
|
1133
|
+
at: clampedAt,
|
|
1134
|
+
furthest: Math.min(Math.max(clampedAt, typeof furthest === "number" && Number.isFinite(furthest) ? furthest : clampedAt), end)
|
|
1135
|
+
};
|
|
1627
1136
|
}
|
|
1137
|
+
/**
|
|
1138
|
+
* One interactive video's composed result: the scored items whose slot belongs
|
|
1139
|
+
* to the entry, composed as one section of weight 1. A slot belongs when
|
|
1140
|
+
* `entryKeyOf(item.slotId) === entryKey`, so a host can pass a whole attempt's
|
|
1141
|
+
* items and get the video's share of it.
|
|
1142
|
+
*
|
|
1143
|
+
* `entryKey` is the group's `slotKey` when it has one — which every published
|
|
1144
|
+
* video should — or its position in the sequence otherwise. A group object
|
|
1145
|
+
* alone cannot know its position, which is why this takes the key.
|
|
1146
|
+
*
|
|
1147
|
+
* @throws Error when no item belongs to the entry. A mistyped key would
|
|
1148
|
+
* otherwise compose nothing into a `final` result, and a refusal must
|
|
1149
|
+
* never read as a grade.
|
|
1150
|
+
*/
|
|
1628
1151
|
function composeTimelineScore(entryKey, items, policy) {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1152
|
+
const own = items.filter((item) => entryKeyOf(item.slotId) === entryKey);
|
|
1153
|
+
if (own.length === 0) throw new Error(`composeTimelineScore: no scored item belongs to entry "${entryKey}". Pass the video's slotKey (or its position in the sequence), and the items of the attempt that holds it.`);
|
|
1154
|
+
return require_scoring.composeAssessmentScore([{
|
|
1155
|
+
id: entryKey,
|
|
1156
|
+
weight: 1,
|
|
1157
|
+
items: own
|
|
1158
|
+
}], policy);
|
|
1636
1159
|
}
|
|
1160
|
+
//#endregion
|
|
1161
|
+
exports.AI_COACHING_MAX_TIP_LENGTH = require_ai_writing.AI_COACHING_MAX_TIP_LENGTH;
|
|
1162
|
+
exports.AI_COACHING_MAX_WORDS = require_ai_writing.AI_COACHING_MAX_WORDS;
|
|
1163
|
+
exports.AI_CRITIQUE_MAX_FIELD_LENGTH = require_ai_writing.AI_CRITIQUE_MAX_FIELD_LENGTH;
|
|
1164
|
+
exports.AI_CRITIQUE_MAX_FINDINGS = require_ai_writing.AI_CRITIQUE_MAX_FINDINGS;
|
|
1165
|
+
exports.AI_DRAFTS_MAX_COUNT = require_ai_writing.AI_DRAFTS_MAX_COUNT;
|
|
1166
|
+
exports.AI_DRAFTS_MAX_REPAIRS = require_ai_writing.AI_DRAFTS_MAX_REPAIRS;
|
|
1167
|
+
exports.AI_EXPLANATION_TYPES = require_ai_writing.AI_EXPLANATION_TYPES;
|
|
1168
|
+
exports.AI_HINT_TYPES = require_ai_writing.AI_HINT_TYPES;
|
|
1169
|
+
exports.AI_TEXT_MAX_LENGTH = require_ai_writing.AI_TEXT_MAX_LENGTH;
|
|
1170
|
+
exports.AI_WRITING_MAX_CORRECTIONS = require_ai_writing.AI_WRITING_MAX_CORRECTIONS;
|
|
1171
|
+
exports.AI_WRITING_MAX_FIELD_LENGTH = require_ai_writing.AI_WRITING_MAX_FIELD_LENGTH;
|
|
1172
|
+
exports.ActivitySchemaError = require_registry.ActivitySchemaError;
|
|
1173
|
+
exports.BUILT_IN_ACTIVITY_TYPES = require_registry.BUILT_IN_ACTIVITY_TYPES;
|
|
1174
|
+
exports.DEFAULT_ITEM_SCORING_POLICY = require_scoring.DEFAULT_ITEM_SCORING_POLICY;
|
|
1175
|
+
exports.DEFAULT_PASS_THRESHOLD = require_scoring.DEFAULT_PASS_THRESHOLD;
|
|
1176
|
+
exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = require_registry.DICTATION_MAX_ACCEPTED_TRANSCRIPTS;
|
|
1177
|
+
exports.DICTATION_MAX_EQUIVALENCES = require_registry.DICTATION_MAX_EQUIVALENCES;
|
|
1178
|
+
exports.DICTATION_MAX_EQUIVALENCE_LENGTH = require_registry.DICTATION_MAX_EQUIVALENCE_LENGTH;
|
|
1179
|
+
exports.DICTATION_MAX_TEXT_LENGTH = require_registry.DICTATION_MAX_TEXT_LENGTH;
|
|
1180
|
+
exports.DICTATION_MAX_TRANSCRIPT_LENGTH = require_registry.DICTATION_MAX_TRANSCRIPT_LENGTH;
|
|
1181
|
+
exports.DeferredScoringError = require_registry.DeferredScoringError;
|
|
1182
|
+
exports.INTERACTIVE_VIDEO_ITEM_TYPES = require_item_group.INTERACTIVE_VIDEO_ITEM_TYPES;
|
|
1183
|
+
exports.ITEM_SCORING_MAX_RETRIES = require_scoring.ITEM_SCORING_MAX_RETRIES;
|
|
1184
|
+
exports.OPEN_DELIVERY_POLICY = OPEN_DELIVERY_POLICY;
|
|
1185
|
+
exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = require_registry.READ_ALOUD_MAX_DIMENSION_WEIGHT;
|
|
1186
|
+
exports.READ_ALOUD_MAX_REFERENCE_LENGTH = require_registry.READ_ALOUD_MAX_REFERENCE_LENGTH;
|
|
1187
|
+
exports.READ_ALOUD_MAX_SECONDS = require_registry.READ_ALOUD_MAX_SECONDS;
|
|
1188
|
+
exports.READ_ALOUD_MAX_TAKES = require_registry.READ_ALOUD_MAX_TAKES;
|
|
1189
|
+
exports.RedactedScoringError = require_registry.RedactedScoringError;
|
|
1190
|
+
exports.SPEECH_ASSESSMENT_MAX_WORDS = require_registry.SPEECH_ASSESSMENT_MAX_WORDS;
|
|
1191
|
+
exports.TIMELINE_MAX_CHAPTERS = require_item_group.TIMELINE_MAX_CHAPTERS;
|
|
1192
|
+
exports.TIMELINE_MAX_ITEMS = require_item_group.TIMELINE_MAX_ITEMS;
|
|
1193
|
+
exports.TIMELINE_MAX_QUIZZES = require_item_group.TIMELINE_MAX_QUIZZES;
|
|
1194
|
+
exports.TIMELINE_MAX_TITLE_LENGTH = require_item_group.TIMELINE_MAX_TITLE_LENGTH;
|
|
1195
|
+
exports.UnknownActivityTypeError = require_registry.UnknownActivityTypeError;
|
|
1196
|
+
exports.XAPIVerb = require_xapi.XAPIVerb;
|
|
1197
|
+
exports.XAPI_VERB_DISPLAY = require_xapi.XAPI_VERB_DISPLAY;
|
|
1198
|
+
exports.aiAllowedByContent = require_ai_writing.aiAllowedByContent;
|
|
1199
|
+
exports.aiCoachingRequest = require_ai_writing.aiCoachingRequest;
|
|
1200
|
+
exports.aiCritiqueRequest = require_ai_writing.aiCritiqueRequest;
|
|
1201
|
+
exports.aiDraftsRepairRequest = require_ai_writing.aiDraftsRepairRequest;
|
|
1202
|
+
exports.aiDraftsRequest = require_ai_writing.aiDraftsRequest;
|
|
1203
|
+
exports.aiExplanationRequest = require_ai_writing.aiExplanationRequest;
|
|
1204
|
+
exports.aiGradeOf = require_ai_writing.aiGradeOf;
|
|
1205
|
+
exports.aiHintRequest = require_ai_writing.aiHintRequest;
|
|
1206
|
+
exports.aiSupports = require_ai_writing.aiSupports;
|
|
1207
|
+
exports.aiWritingFeedbackRequest = require_ai_writing.aiWritingFeedbackRequest;
|
|
1208
|
+
exports.alignDictation = require_registry.alignDictation;
|
|
1209
|
+
exports.alignReadAloud = require_scoring.alignReadAloud;
|
|
1210
|
+
exports.assertRedacted = assertRedacted;
|
|
1211
|
+
exports.assertRedactedItemGroup = assertRedactedItemGroup;
|
|
1212
|
+
exports.buildAiFacts = require_ai_writing.buildAiFacts;
|
|
1213
|
+
exports.canonicalJson = canonicalJson;
|
|
1214
|
+
exports.checkAiCoaching = require_ai_writing.checkAiCoaching;
|
|
1215
|
+
exports.checkAiCritique = require_ai_writing.checkAiCritique;
|
|
1216
|
+
exports.checkAiDrafts = require_ai_writing.checkAiDrafts;
|
|
1217
|
+
exports.checkAiExplanation = require_ai_writing.checkAiExplanation;
|
|
1218
|
+
exports.checkAiHint = require_ai_writing.checkAiHint;
|
|
1219
|
+
exports.checkAiWritingFeedback = require_ai_writing.checkAiWritingFeedback;
|
|
1220
|
+
exports.classifyBand = require_scoring.classifyBand;
|
|
1221
|
+
exports.combineDeliveryPolicies = combineDeliveryPolicies;
|
|
1222
|
+
exports.composeAssessmentScore = require_scoring.composeAssessmentScore;
|
|
1223
|
+
exports.composeTimelineScore = composeTimelineScore;
|
|
1224
|
+
exports.computePassThreshold = require_scoring.computePassThreshold;
|
|
1225
|
+
exports.contentHash = contentHash;
|
|
1226
|
+
exports.countWords = require_registry.countWords;
|
|
1227
|
+
exports.createDraft = require_ai_writing.createDraft;
|
|
1228
|
+
exports.createInteractiveVideoDraft = require_ai_writing.createInteractiveVideoDraft;
|
|
1229
|
+
exports.createItemGroupDraft = require_ai_writing.createItemGroupDraft;
|
|
1230
|
+
exports.critiqueDraft = require_ai_writing.critiqueDraft;
|
|
1231
|
+
exports.critiqueDrafts = require_ai_writing.critiqueDrafts;
|
|
1232
|
+
exports.critiqueItemGroupDraft = require_ai_writing.critiqueItemGroupDraft;
|
|
1233
|
+
exports.defineActivityType = require_registry.defineActivityType;
|
|
1234
|
+
exports.dictationJsonSchema = require_schemas.dictationJsonSchema;
|
|
1235
|
+
exports.dictationReferenceWords = require_registry.dictationReferenceWords;
|
|
1236
|
+
exports.dictationType = require_registry.dictationType;
|
|
1237
|
+
exports.diffDictationChars = require_scoring.diffDictationChars;
|
|
1238
|
+
exports.diffResponses = diffResponses;
|
|
1239
|
+
exports.entryKeyOf = entryKeyOf;
|
|
1240
|
+
exports.evaluate = require_scoring.evaluate;
|
|
1241
|
+
exports.evaluateTries = require_scoring.evaluateTries;
|
|
1242
|
+
exports.fillInTheBlanksJsonSchema = require_schemas.fillInTheBlanksJsonSchema;
|
|
1243
|
+
exports.fillInTheBlanksType = require_registry.fillInTheBlanksType;
|
|
1244
|
+
exports.fingerprint = fingerprint;
|
|
1245
|
+
exports.flattenSequence = flattenSequence;
|
|
1246
|
+
exports.gapSelectJsonSchema = require_schemas.gapSelectJsonSchema;
|
|
1247
|
+
exports.gapSelectType = require_registry.gapSelectType;
|
|
1248
|
+
exports.generateDrafts = require_ai_writing.generateDrafts;
|
|
1249
|
+
exports.getActivityTypeDescriptor = require_registry.getActivityTypeDescriptor;
|
|
1250
|
+
exports.gradeFromRubric = require_scoring.gradeFromRubric;
|
|
1251
|
+
exports.gradeReadAloud = require_scoring.gradeReadAloud;
|
|
1252
|
+
exports.gte = require_scoring.gte;
|
|
1253
|
+
exports.hasGrade = require_scoring.hasGrade;
|
|
1254
|
+
exports.hashSeed = hashSeed;
|
|
1255
|
+
exports.hintRevealsAnswer = require_ai_writing.hintRevealsAnswer;
|
|
1256
|
+
exports.inspectWav = require_scoring.inspectWav;
|
|
1257
|
+
exports.interactiveVideoFromDrafts = require_ai_writing.interactiveVideoFromDrafts;
|
|
1258
|
+
exports.isBuiltInActivityType = require_registry.isBuiltInActivityType;
|
|
1259
|
+
exports.isInteractiveVideoItemType = require_item_group.isInteractiveVideoItemType;
|
|
1260
|
+
exports.isItemGroup = isItemGroup;
|
|
1261
|
+
exports.itemGroupJsonSchema = require_schemas.itemGroupJsonSchema;
|
|
1262
|
+
exports.jsonSchemaFor = require_schemas.jsonSchemaFor;
|
|
1263
|
+
exports.levenshteinDistance = require_registry.levenshteinDistance;
|
|
1264
|
+
exports.matchText = require_registry.matchText;
|
|
1265
|
+
exports.multipleChoiceJsonSchema = require_schemas.multipleChoiceJsonSchema;
|
|
1266
|
+
exports.multipleChoiceType = require_registry.multipleChoiceType;
|
|
1267
|
+
exports.outcomeFromGrade = require_scoring.outcomeFromGrade;
|
|
1268
|
+
exports.outcomeFromUnscorable = require_scoring.outcomeFromUnscorable;
|
|
1269
|
+
exports.planAttempt = planAttempt;
|
|
1270
|
+
exports.planMediaBudgets = planMediaBudgets;
|
|
1271
|
+
exports.readAiTextResult = require_ai_writing.readAiTextResult;
|
|
1272
|
+
exports.readAloudJsonSchema = require_schemas.readAloudJsonSchema;
|
|
1273
|
+
exports.readAloudType = require_registry.readAloudType;
|
|
1274
|
+
exports.readMediaProgress = readMediaProgress;
|
|
1275
|
+
exports.redact = redact;
|
|
1276
|
+
exports.redactItemGroup = redactItemGroup;
|
|
1277
|
+
exports.registerActivityType = require_registry.registerActivityType;
|
|
1278
|
+
exports.registeredActivityTypes = require_registry.registeredActivityTypes;
|
|
1279
|
+
exports.resolveDeliveryPolicy = resolveDeliveryPolicy;
|
|
1280
|
+
exports.resolveItemScoringPolicy = require_scoring.resolveItemScoringPolicy;
|
|
1281
|
+
exports.resolvePlaybackPolicy = resolvePlaybackPolicy;
|
|
1282
|
+
exports.restoreAttemptState = restoreAttemptState;
|
|
1283
|
+
exports.restoreMediaPlayLedger = restoreMediaPlayLedger;
|
|
1284
|
+
exports.roundGrade = require_scoring.roundGrade;
|
|
1285
|
+
exports.score = require_scoring.score;
|
|
1286
|
+
exports.scoreTries = require_scoring.scoreTries;
|
|
1287
|
+
exports.scoredItemsFromPlan = scoredItemsFromPlan;
|
|
1288
|
+
exports.seededShuffle = seededShuffle;
|
|
1289
|
+
exports.serializeAttemptState = serializeAttemptState;
|
|
1290
|
+
exports.serializeMediaPlayLedger = serializeMediaPlayLedger;
|
|
1291
|
+
exports.slotMediaKey = slotMediaKey;
|
|
1292
|
+
exports.stimulusJsonSchema = require_schemas.stimulusJsonSchema;
|
|
1293
|
+
exports.stimulusMediaKey = stimulusMediaKey;
|
|
1294
|
+
exports.validateActivity = require_schemas.validateActivity;
|
|
1295
|
+
exports.validateDeliveryPolicy = validateDeliveryPolicy;
|
|
1296
|
+
exports.validateDraft = require_ai_writing.validateDraft;
|
|
1297
|
+
exports.validateItemGroup = require_item_group.validateItemGroup;
|
|
1298
|
+
exports.validateItemGroupDraft = require_ai_writing.validateItemGroupDraft;
|
|
1299
|
+
exports.validateItemScoringPolicy = require_scoring.validateItemScoringPolicy;
|
|
1300
|
+
exports.validateMedia = require_schemas.validateMedia;
|
|
1301
|
+
exports.validateOptionMedia = require_schemas.validateOptionMedia;
|
|
1302
|
+
exports.validateSpeechAssessment = require_scoring.validateSpeechAssessment;
|
|
1303
|
+
exports.validateXAPIStatement = require_xapi.validateXAPIStatement;
|
|
1304
|
+
exports.verifyAttemptPlan = verifyAttemptPlan;
|
|
1305
|
+
exports.writtenResponseJsonSchema = require_schemas.writtenResponseJsonSchema;
|
|
1306
|
+
exports.writtenResponseType = require_registry.writtenResponseType;
|
|
1307
|
+
exports.xAPIBuilder = require_xapi.xAPIBuilder;
|
|
1308
|
+
exports.xapiDefinitionFor = require_xapi.xapiDefinitionFor;
|
|
1637
1309
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
exports.AI_COACHING_MAX_TIP_LENGTH = _chunkLDFQUZIIcjs.AI_COACHING_MAX_TIP_LENGTH; exports.AI_COACHING_MAX_WORDS = _chunkLDFQUZIIcjs.AI_COACHING_MAX_WORDS; exports.AI_EXPLANATION_TYPES = _chunkLDFQUZIIcjs.AI_EXPLANATION_TYPES; exports.AI_HINT_TYPES = _chunkLDFQUZIIcjs.AI_HINT_TYPES; exports.AI_TEXT_MAX_LENGTH = _chunkLDFQUZIIcjs.AI_TEXT_MAX_LENGTH; exports.AI_WRITING_MAX_CORRECTIONS = _chunkLDFQUZIIcjs.AI_WRITING_MAX_CORRECTIONS; exports.AI_WRITING_MAX_FIELD_LENGTH = _chunkLDFQUZIIcjs.AI_WRITING_MAX_FIELD_LENGTH; exports.ActivitySchemaError = _chunkDSDXL3JOcjs.ActivitySchemaError; exports.DEFAULT_ITEM_SCORING_POLICY = _chunkVSH7ECQLcjs.DEFAULT_ITEM_SCORING_POLICY; exports.DEFAULT_PASS_THRESHOLD = _chunkVSH7ECQLcjs.DEFAULT_PASS_THRESHOLD; exports.DICTATION_MAX_ACCEPTED_TRANSCRIPTS = _chunkDSDXL3JOcjs.DICTATION_MAX_ACCEPTED_TRANSCRIPTS; exports.DICTATION_MAX_EQUIVALENCES = _chunkDSDXL3JOcjs.DICTATION_MAX_EQUIVALENCES; exports.DICTATION_MAX_EQUIVALENCE_LENGTH = _chunkDSDXL3JOcjs.DICTATION_MAX_EQUIVALENCE_LENGTH; exports.DICTATION_MAX_TEXT_LENGTH = _chunkDSDXL3JOcjs.DICTATION_MAX_TEXT_LENGTH; exports.DICTATION_MAX_TRANSCRIPT_LENGTH = _chunkDSDXL3JOcjs.DICTATION_MAX_TRANSCRIPT_LENGTH; exports.DeferredScoringError = _chunkDSDXL3JOcjs.DeferredScoringError; exports.INTERACTIVE_VIDEO_ITEM_TYPES = _chunk2JT2WUQ2cjs.INTERACTIVE_VIDEO_ITEM_TYPES; exports.ITEM_SCORING_MAX_RETRIES = _chunkVSH7ECQLcjs.ITEM_SCORING_MAX_RETRIES; exports.OPEN_DELIVERY_POLICY = OPEN_DELIVERY_POLICY; exports.READ_ALOUD_MAX_DIMENSION_WEIGHT = _chunkDSDXL3JOcjs.READ_ALOUD_MAX_DIMENSION_WEIGHT; exports.READ_ALOUD_MAX_REFERENCE_LENGTH = _chunkDSDXL3JOcjs.READ_ALOUD_MAX_REFERENCE_LENGTH; exports.READ_ALOUD_MAX_SECONDS = _chunkDSDXL3JOcjs.READ_ALOUD_MAX_SECONDS; exports.READ_ALOUD_MAX_TAKES = _chunkDSDXL3JOcjs.READ_ALOUD_MAX_TAKES; exports.RedactedScoringError = _chunkDSDXL3JOcjs.RedactedScoringError; exports.SPEECH_ASSESSMENT_MAX_WORDS = _chunkDSDXL3JOcjs.SPEECH_ASSESSMENT_MAX_WORDS; exports.TIMELINE_MAX_CHAPTERS = _chunk2JT2WUQ2cjs.TIMELINE_MAX_CHAPTERS; exports.TIMELINE_MAX_ITEMS = _chunk2JT2WUQ2cjs.TIMELINE_MAX_ITEMS; exports.TIMELINE_MAX_QUIZZES = _chunk2JT2WUQ2cjs.TIMELINE_MAX_QUIZZES; exports.TIMELINE_MAX_TITLE_LENGTH = _chunk2JT2WUQ2cjs.TIMELINE_MAX_TITLE_LENGTH; exports.UnknownActivityTypeError = _chunkDSDXL3JOcjs.UnknownActivityTypeError; exports.XAPIVerb = _chunkTJJFKKH2cjs.XAPIVerb; exports.XAPI_VERB_DISPLAY = _chunkTJJFKKH2cjs.XAPI_VERB_DISPLAY; exports.aiAllowedByContent = _chunkLDFQUZIIcjs.aiAllowedByContent; exports.aiCoachingRequest = _chunkLDFQUZIIcjs.aiCoachingRequest; exports.aiExplanationRequest = _chunkLDFQUZIIcjs.aiExplanationRequest; exports.aiGradeOf = _chunkLDFQUZIIcjs.aiGradeOf; exports.aiHintRequest = _chunkLDFQUZIIcjs.aiHintRequest; exports.aiSupports = _chunkLDFQUZIIcjs.aiSupports; exports.aiWritingFeedbackRequest = _chunkLDFQUZIIcjs.aiWritingFeedbackRequest; exports.alignDictation = _chunkDSDXL3JOcjs.alignDictation; exports.alignReadAloud = _chunkVSH7ECQLcjs.alignReadAloud; exports.assertRedacted = assertRedacted; exports.assertRedactedItemGroup = assertRedactedItemGroup; exports.buildAiFacts = _chunkLDFQUZIIcjs.buildAiFacts; exports.canonicalJson = canonicalJson; exports.checkAiCoaching = _chunkLDFQUZIIcjs.checkAiCoaching; exports.checkAiExplanation = _chunkLDFQUZIIcjs.checkAiExplanation; exports.checkAiHint = _chunkLDFQUZIIcjs.checkAiHint; exports.checkAiWritingFeedback = _chunkLDFQUZIIcjs.checkAiWritingFeedback; exports.classifyBand = _chunkVSH7ECQLcjs.classifyBand; exports.combineDeliveryPolicies = combineDeliveryPolicies; exports.composeAssessmentScore = _chunkVSH7ECQLcjs.composeAssessmentScore; exports.composeTimelineScore = composeTimelineScore; exports.computePassThreshold = _chunkVSH7ECQLcjs.computePassThreshold; exports.contentHash = contentHash; exports.countWords = _chunkDSDXL3JOcjs.countWords; exports.createDraft = createDraft; exports.createInteractiveVideoDraft = createInteractiveVideoDraft; exports.createItemGroupDraft = createItemGroupDraft; exports.defineActivityType = _chunkDSDXL3JOcjs.defineActivityType; exports.dictationJsonSchema = _chunk2JT2WUQ2cjs.dictationJsonSchema; exports.dictationReferenceWords = _chunkDSDXL3JOcjs.dictationReferenceWords; exports.dictationType = _chunkDSDXL3JOcjs.dictationType; exports.diffDictationChars = _chunkVSH7ECQLcjs.diffDictationChars; exports.diffResponses = diffResponses; exports.entryKeyOf = entryKeyOf; exports.evaluate = _chunkVSH7ECQLcjs.evaluate; exports.evaluateTries = _chunkVSH7ECQLcjs.evaluateTries; exports.fillInTheBlanksJsonSchema = _chunk2JT2WUQ2cjs.fillInTheBlanksJsonSchema; exports.fillInTheBlanksType = _chunkDSDXL3JOcjs.fillInTheBlanksType; exports.fingerprint = fingerprint; exports.flattenSequence = flattenSequence; exports.gapSelectJsonSchema = _chunk2JT2WUQ2cjs.gapSelectJsonSchema; exports.gapSelectType = _chunkDSDXL3JOcjs.gapSelectType; exports.getActivityTypeDescriptor = _chunkDSDXL3JOcjs.getActivityTypeDescriptor; exports.gradeFromRubric = _chunkVSH7ECQLcjs.gradeFromRubric; exports.gradeReadAloud = _chunkVSH7ECQLcjs.gradeReadAloud; exports.gte = _chunkVSH7ECQLcjs.gte; exports.hasGrade = _chunkVSH7ECQLcjs.hasGrade; exports.hashSeed = hashSeed; exports.hintRevealsAnswer = _chunkLDFQUZIIcjs.hintRevealsAnswer; exports.inspectWav = _chunkVSH7ECQLcjs.inspectWav; exports.isInteractiveVideoItemType = _chunk2JT2WUQ2cjs.isInteractiveVideoItemType; exports.isItemGroup = isItemGroup; exports.itemGroupJsonSchema = _chunk2JT2WUQ2cjs.itemGroupJsonSchema; exports.jsonSchemaFor = _chunk2JT2WUQ2cjs.jsonSchemaFor; exports.levenshteinDistance = _chunkDSDXL3JOcjs.levenshteinDistance; exports.matchText = _chunkDSDXL3JOcjs.matchText; exports.multipleChoiceJsonSchema = _chunk2JT2WUQ2cjs.multipleChoiceJsonSchema; exports.multipleChoiceType = _chunkDSDXL3JOcjs.multipleChoiceType; exports.outcomeFromGrade = _chunkVSH7ECQLcjs.outcomeFromGrade; exports.outcomeFromUnscorable = _chunkVSH7ECQLcjs.outcomeFromUnscorable; exports.planAttempt = planAttempt; exports.planMediaBudgets = planMediaBudgets; exports.readAiTextResult = _chunkLDFQUZIIcjs.readAiTextResult; exports.readAloudJsonSchema = _chunk2JT2WUQ2cjs.readAloudJsonSchema; exports.readAloudType = _chunkDSDXL3JOcjs.readAloudType; exports.readMediaProgress = readMediaProgress; exports.redact = redact; exports.redactItemGroup = redactItemGroup; exports.registerActivityType = _chunkDSDXL3JOcjs.registerActivityType; exports.registeredActivityTypes = _chunkDSDXL3JOcjs.registeredActivityTypes; exports.resolveDeliveryPolicy = resolveDeliveryPolicy; exports.resolveItemScoringPolicy = _chunkVSH7ECQLcjs.resolveItemScoringPolicy; exports.resolvePlaybackPolicy = resolvePlaybackPolicy; exports.restoreAttemptState = restoreAttemptState; exports.restoreMediaPlayLedger = restoreMediaPlayLedger; exports.roundGrade = _chunkVSH7ECQLcjs.roundGrade; exports.score = _chunkVSH7ECQLcjs.score; exports.scoreTries = _chunkVSH7ECQLcjs.scoreTries; exports.scoredItemsFromPlan = scoredItemsFromPlan; exports.seededShuffle = seededShuffle; exports.serializeAttemptState = serializeAttemptState; exports.serializeMediaPlayLedger = serializeMediaPlayLedger; exports.slotMediaKey = slotMediaKey; exports.stimulusJsonSchema = _chunk2JT2WUQ2cjs.stimulusJsonSchema; exports.stimulusMediaKey = stimulusMediaKey; exports.validateActivity = _chunk2JT2WUQ2cjs.validateActivity; exports.validateDeliveryPolicy = validateDeliveryPolicy; exports.validateDraft = validateDraft; exports.validateItemGroup = _chunk2JT2WUQ2cjs.validateItemGroup; exports.validateItemGroupDraft = validateItemGroupDraft; exports.validateItemScoringPolicy = _chunkVSH7ECQLcjs.validateItemScoringPolicy; exports.validateMedia = _chunk2JT2WUQ2cjs.validateMedia; exports.validateOptionMedia = _chunk2JT2WUQ2cjs.validateOptionMedia; exports.validateSpeechAssessment = _chunkVSH7ECQLcjs.validateSpeechAssessment; exports.validateXAPIStatement = _chunkTJJFKKH2cjs.validateXAPIStatement; exports.verifyAttemptPlan = verifyAttemptPlan; exports.writtenResponseJsonSchema = _chunk2JT2WUQ2cjs.writtenResponseJsonSchema; exports.writtenResponseType = _chunkDSDXL3JOcjs.writtenResponseType; exports.xAPIBuilder = _chunkTJJFKKH2cjs.xAPIBuilder; exports.xapiDefinitionFor = _chunkTJJFKKH2cjs.xapiDefinitionFor;
|
|
1771
1310
|
//# sourceMappingURL=index.cjs.map
|