@bitflow/core 0.5.4 → 0.7.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/dist/index.js ADDED
@@ -0,0 +1,1410 @@
1
+ import { z as o } from "zod";
2
+ const rt = [
3
+ "INVALID_FLOW",
4
+ "INVALID_ATTEMPT",
5
+ "FLOW_ATTEMPT_MISMATCH",
6
+ "UNKNOWN_BIT_TYPE",
7
+ "LOAD_FAILED",
8
+ "EVALUATION_FAILED"
9
+ ], w = (e, t, n) => n && n.length > 0 ? { code: e, message: t, diagnostics: n } : { code: e, message: t }, _ = (e) => e.map((t) => ({
10
+ path: t.path.map(String).join("."),
11
+ message: t.message
12
+ })), Y = (e) => e.score ? e.score : e.state === "unknown" ? { earned: 0, possible: 0 } : { earned: e.state === "correct" ? 1 : 0, possible: 1 }, j = (e) => {
13
+ let t = 0, n = 0;
14
+ for (const s of e) {
15
+ const r = Y(s);
16
+ t += r.earned, n += r.possible;
17
+ }
18
+ return { earned: t, possible: n };
19
+ }, V = (e, t) => {
20
+ if (!t) return e;
21
+ let n = e;
22
+ for (const s of t.split(".")) {
23
+ if (n == null || typeof n != "object") return;
24
+ n = n[s];
25
+ }
26
+ return n;
27
+ }, ce = (e, t) => {
28
+ if (e)
29
+ switch (e.kind) {
30
+ case "nodes":
31
+ return e.nodeIds;
32
+ case "section":
33
+ return Object.keys(t.sections).filter(
34
+ (n) => t.sections[n] === e.id
35
+ );
36
+ case "last": {
37
+ const n = [];
38
+ for (let s = t.history.length - 1; s >= 0 && n.length < e.count; s--) {
39
+ const r = t.history[s];
40
+ n.includes(r) || r in t.results && n.push(r);
41
+ }
42
+ return n;
43
+ }
44
+ }
45
+ }, M = (e, t) => {
46
+ const n = ce(e, t);
47
+ return n ? n.map((s) => t.results[s]).filter((s) => s !== void 0) : Object.values(t.results);
48
+ }, le = (e, t) => {
49
+ switch (e.kind) {
50
+ case "answer":
51
+ return V(t.answers[e.nodeId], e.path);
52
+ case "result":
53
+ return V(t.results[e.nodeId], e.path);
54
+ case "tries":
55
+ return t.tries[e.nodeId] ?? 0;
56
+ case "visits":
57
+ return t.visits[e.nodeId] ?? 0;
58
+ case "confidence":
59
+ return t.confidence[e.nodeId];
60
+ case "timeSpent":
61
+ return e.nodeId === void 0 ? t.totalTimeSpent : t.timeSpent[e.nodeId] ?? 0;
62
+ case "timeRemaining":
63
+ return t.timeRemaining ?? void 0;
64
+ case "score":
65
+ return j(M(e.scope, t)).earned;
66
+ case "scoreRatio": {
67
+ const n = j(M(e.scope, t));
68
+ return n.possible === 0 ? 0 : n.earned / n.possible;
69
+ }
70
+ case "resultCount":
71
+ return M(e.scope, t).filter(
72
+ (n) => n.state === e.state
73
+ ).length;
74
+ }
75
+ }, W = (e) => typeof e == "number" ? e : Number(e), $ = (e, t) => {
76
+ switch (e.type) {
77
+ case "always":
78
+ return !0;
79
+ case "not":
80
+ return !$(e.condition, t);
81
+ case "and":
82
+ return e.conditions.every((n) => $(n, t));
83
+ case "or":
84
+ return e.conditions.some((n) => $(n, t));
85
+ case "compare":
86
+ return ue(
87
+ le(e.left, t),
88
+ e.op,
89
+ e.right
90
+ );
91
+ }
92
+ }, ue = (e, t, n) => {
93
+ switch (t) {
94
+ case "isTrue":
95
+ return e === !0;
96
+ case "eq":
97
+ return e === n;
98
+ case "ne":
99
+ return e !== n;
100
+ case "in":
101
+ return Array.isArray(n) ? n.includes(e) : !1;
102
+ case "notIn":
103
+ return Array.isArray(n) ? !n.includes(e) : !1;
104
+ case "gt":
105
+ case "gte":
106
+ case "lt":
107
+ case "lte": {
108
+ if (e == null) return !1;
109
+ const s = W(e), r = W(n);
110
+ return Number.isNaN(s) || Number.isNaN(r) ? !1 : t === "gt" ? s > r : t === "gte" ? s >= r : t === "lt" ? s < r : s <= r;
111
+ }
112
+ default:
113
+ return !1;
114
+ }
115
+ }, L = (e) => {
116
+ switch (e.type) {
117
+ case "always":
118
+ return [];
119
+ case "not":
120
+ return L(e.condition);
121
+ case "and":
122
+ case "or":
123
+ return e.conditions.flatMap(L);
124
+ case "compare": {
125
+ const { left: t } = e;
126
+ return "nodeId" in t && t.nodeId !== void 0 ? [t.nodeId] : "scope" in t && t.scope?.kind === "nodes" ? t.scope.nodeIds : [];
127
+ }
128
+ }
129
+ }, U = (e) => {
130
+ switch (e.type) {
131
+ case "always":
132
+ return [];
133
+ case "not":
134
+ return U(e.condition);
135
+ case "and":
136
+ case "or":
137
+ return e.conditions.flatMap(U);
138
+ case "compare": {
139
+ const { left: t } = e;
140
+ return "scope" in t && t.scope?.kind === "section" ? [t.scope.id] : [];
141
+ }
142
+ }
143
+ }, it = ["start", "content", "task", "end"], I = /* @__PURE__ */ new Map(), at = (e) => {
144
+ I.set(e.type, e);
145
+ }, b = (e) => I.get(e), ct = (e) => I.has(e), de = () => Array.from(I.values()), lt = () => {
146
+ I.clear();
147
+ }, p = (e, t) => e.nodes.find((n) => n.id === t), q = (e, t) => {
148
+ const n = +!!t.condition - +!!e.condition;
149
+ if (n !== 0) return n;
150
+ const s = (e.sourceHandle ?? "").localeCompare(t.sourceHandle ?? "");
151
+ return s !== 0 ? s : e.id.localeCompare(t.id);
152
+ }, k = (e, t) => e.edges.filter((n) => n.source === t).sort(q), C = (e, t) => e.edges.filter((n) => n.target === t), fe = (e) => {
153
+ const t = e.nodes.find((s) => b(s.type)?.kind === "start");
154
+ return t ? t.id : (e.nodes.find((s) => C(e, s.id).length === 0) ?? e.nodes[0])?.id;
155
+ }, G = (e, t) => e.nodes.filter((n) => n.pool === t), me = (e, t = Math.random) => {
156
+ const n = {};
157
+ for (const s of e.meta.pools)
158
+ n[s.id] = he(
159
+ G(e, s.id).map((r) => r.id),
160
+ t
161
+ ).slice(0, s.draw);
162
+ return n;
163
+ }, he = (e, t) => {
164
+ const n = [...e];
165
+ for (let s = n.length - 1; s > 0; s--) {
166
+ const r = Math.floor(t() * (s + 1));
167
+ [n[s], n[r]] = [n[r], n[s]];
168
+ }
169
+ return n;
170
+ }, K = (e, t) => {
171
+ if (!t?.pool) return !0;
172
+ const n = e.pools[t.pool];
173
+ return n === void 0 || n.includes(t.id);
174
+ }, Q = (e, t) => {
175
+ const n = p(e, t);
176
+ return !n || b(n.type)?.kind === "end" ? !0 : k(e, t).length === 0;
177
+ }, X = (e, t) => {
178
+ if (!t?.pool) return;
179
+ const n = e.meta.pools.find((s) => s.id === t.pool);
180
+ return n?.shuffle ? n : void 0;
181
+ }, Z = (e, t) => e.edges.filter((n) => {
182
+ const s = p(e, n.source), r = p(e, n.target);
183
+ return s?.pool === t && r !== void 0 && r.pool !== t;
184
+ }).sort(q), x = (e, t, n, s = {}) => {
185
+ const { isActive: r = () => !0, order: i = {} } = s;
186
+ let a = t;
187
+ for (let u = 0; u <= e.nodes.length; u++) {
188
+ const f = p(e, a);
189
+ if (!f || b(f.type)?.kind === "end") return null;
190
+ const d = pe(e, f, n, i);
191
+ if (d === null) return null;
192
+ const h = ge(e, f, d, i), T = p(e, h.nodeId);
193
+ if (T && r(T)) return h;
194
+ a = h.nodeId;
195
+ }
196
+ return null;
197
+ }, ut = (e, t, n, s = () => !0, r = {}) => x(e, t, n, { isActive: s, order: r })?.nodeId ?? null, pe = (e, t, n, s) => {
198
+ const r = X(e, t), i = r ? s[r.id] ?? [] : [];
199
+ if (r && i.length > 0) {
200
+ const a = i.indexOf(t.id);
201
+ return a >= 0 && a + 1 < i.length ? { nodeId: i[a + 1] } : H(e, Z(e, r.id), n);
202
+ }
203
+ return H(e, k(e, t.id), n);
204
+ }, ge = (e, t, n, s) => {
205
+ const r = X(e, p(e, n.nodeId));
206
+ if (!r || t.pool === r.id) return n;
207
+ const i = s[r.id] ?? [];
208
+ return i.length === 0 ? n : { ...n, nodeId: i[0] };
209
+ }, H = (e, t, n) => {
210
+ for (const s of t)
211
+ if ((!s.condition || $(s.condition, n)) && p(e, s.target))
212
+ return { nodeId: s.target, edge: s };
213
+ return null;
214
+ }, ee = (e) => {
215
+ const { history: t } = e;
216
+ return t.length >= 2 ? t[t.length - 2] : null;
217
+ }, dt = (e, t) => {
218
+ const n = [], s = /* @__PURE__ */ new Set();
219
+ for (const r of t.history) {
220
+ if (s.has(r)) continue;
221
+ s.add(r);
222
+ const i = p(e, r);
223
+ if (!i) continue;
224
+ const a = b(i.type)?.kind === "task", u = t.results[r] !== void 0;
225
+ n.push({
226
+ nodeId: r,
227
+ node: i,
228
+ position: n.length + 1,
229
+ current: r === t.currentNodeId,
230
+ answered: a && u,
231
+ outstanding: a && !u,
232
+ section: be(e, i)
233
+ });
234
+ }
235
+ return n;
236
+ }, we = (e, t, n) => e.meta.navigation === "free" && t.history.includes(n) && p(e, n) !== void 0, be = (e, t) => t?.section ? e.meta.sections.find((n) => n.id === t.section) : void 0, ye = (e, t) => e.nodes.filter((n) => n.section === t), ft = (e, t) => {
237
+ const n = t?.data?.evaluation;
238
+ return typeof n?.allowSkip == "boolean" ? n.allowSkip : e.meta.allowSkip;
239
+ }, mt = (e, t) => {
240
+ if (!e) return !0;
241
+ const n = b(e.type);
242
+ if (!n?.isComplete) return !0;
243
+ const s = n.schema.safeParse(e.data);
244
+ return s.success ? n.isComplete({ data: s.data, answer: t }) : !0;
245
+ }, ht = (e) => j(Object.values(e.results)), ve = (e, t, n = /* @__PURE__ */ new Date()) => {
246
+ const s = {};
247
+ for (const d of t.history)
248
+ s[d] = (s[d] ?? 0) + 1;
249
+ const r = {};
250
+ for (const [d, h] of Object.entries(t.confidence ?? {}))
251
+ r[d] = h.level;
252
+ const i = {};
253
+ for (const d of /* @__PURE__ */ new Set([
254
+ ...Object.keys(t.elapsedMs),
255
+ t.currentNodeId
256
+ ]))
257
+ i[d] = Ie(t, d, n) / 1e3;
258
+ const a = Se(t, n) / 1e3, u = e.meta.timeLimit ?? null, f = {};
259
+ for (const d of e.nodes)
260
+ d.section && (f[d.id] = d.section);
261
+ return {
262
+ answers: t.answers,
263
+ results: t.results,
264
+ tries: t.tries,
265
+ visits: s,
266
+ confidence: r,
267
+ timeSpent: i,
268
+ totalTimeSpent: a,
269
+ timeRemaining: u === null ? null : Math.max(0, u - a),
270
+ history: t.history,
271
+ sections: f
272
+ };
273
+ }, pt = (e, t) => te(e.answers, t), gt = (e, t) => te(e.results, t), te = (e, t) => {
274
+ if (!t) return { ...e };
275
+ const n = {};
276
+ for (const s of t)
277
+ s in e && (n[s] = e[s]);
278
+ return n;
279
+ }, ke = (e, t, n = "optimistic", s = () => !0) => {
280
+ const r = /* @__PURE__ */ new Set(), i = (a) => {
281
+ if (Q(e, a)) return 0;
282
+ if (r.has(a)) return Number.POSITIVE_INFINITY;
283
+ r.add(a);
284
+ const u = k(e, a).map((f) => {
285
+ const d = p(e, f.target), h = i(f.target);
286
+ return Number.isFinite(h) ? d && !s(d) ? h : h + 1 : h;
287
+ }).filter(Number.isFinite);
288
+ return r.delete(a), u.length === 0 ? Number.POSITIVE_INFINITY : n === "optimistic" ? Math.min(...u) : Math.max(...u);
289
+ };
290
+ return p(e, t) ? i(t) : Number.POSITIVE_INFINITY;
291
+ }, Ie = (e, t, n = /* @__PURE__ */ new Date()) => {
292
+ const s = e.elapsedMs[t] ?? 0;
293
+ return e.currentNodeId !== t || e.status !== "inProgress" ? s : s + ne(e, n);
294
+ }, Se = (e, t = /* @__PURE__ */ new Date()) => {
295
+ const n = Object.values(e.elapsedMs).reduce(
296
+ (s, r) => s + r,
297
+ 0
298
+ );
299
+ return e.status === "inProgress" ? n + ne(e, t) : n;
300
+ }, ne = (e, t) => Math.max(0, t.getTime() - new Date(e.enteredAt).getTime()), wt = (e) => {
301
+ const n = e?.data?.evaluation?.timeLimit;
302
+ return typeof n == "number" && n > 0 ? n * 1e3 : null;
303
+ }, bt = (e, t) => {
304
+ const n = t.history.length;
305
+ if (t.status !== "inProgress")
306
+ return { visited: n, remaining: 0, ratio: 1 };
307
+ const s = ke(
308
+ e,
309
+ t.currentNodeId,
310
+ "optimistic",
311
+ (i) => K(t, i)
312
+ ), r = Number.isFinite(s) ? n + s : n;
313
+ return { visited: n, remaining: s, ratio: r === 0 ? 0 : n / r };
314
+ }, Te = (e) => {
315
+ const t = typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : $e();
316
+ return e ? `${e}-${t}` : t;
317
+ }, $e = () => {
318
+ const e = new Uint8Array(16);
319
+ if (typeof crypto < "u" && typeof crypto.getRandomValues == "function")
320
+ crypto.getRandomValues(e);
321
+ else
322
+ for (let n = 0; n < e.length; n++)
323
+ e[n] = Math.floor(Math.random() * 256);
324
+ e[6] = e[6] & 15 | 64, e[8] = e[8] & 63 | 128;
325
+ const t = Array.from(e, (n) => n.toString(16).padStart(2, "0")).join("");
326
+ return `${t.slice(0, 8)}-${t.slice(8, 12)}-${t.slice(12, 16)}-${t.slice(16, 20)}-${t.slice(20)}`;
327
+ }, Ne = 1, se = [
328
+ "en",
329
+ "de",
330
+ "fr",
331
+ "nl",
332
+ "es",
333
+ "it",
334
+ "pt",
335
+ "tr"
336
+ ], Ae = o.enum(se), P = [
337
+ "correct",
338
+ "wrong",
339
+ /**
340
+ * Not scorable: skipped, or a task whose evaluation is switched off. There is
341
+ * deliberately no "awaiting a teacher" state — bitflow grades in the browser
342
+ * and has no server, so nothing here could ever resolve one.
343
+ */
344
+ "unknown"
345
+ ], oe = o.enum(P), O = o.union([
346
+ /** Everything in one section — see `BitflowMeta.sections`. */
347
+ o.object({ kind: o.literal("section"), id: o.string().min(1) }),
348
+ /** A hand-picked set of steps. */
349
+ o.object({ kind: o.literal("nodes"), nodeIds: o.array(o.string().min(1)) }),
350
+ /**
351
+ * The most recently answered steps, newest first, counted off the attempt's
352
+ * own history. A step answered twice counts once — it is the same task.
353
+ */
354
+ o.object({ kind: o.literal("last"), count: o.number().int().positive() })
355
+ ]), Ee = o.union([
356
+ o.object({
357
+ kind: o.literal("answer"),
358
+ nodeId: o.string().min(1),
359
+ /** Dot path into the answer object, e.g. `"checked.0"`. */
360
+ path: o.string().optional()
361
+ }),
362
+ o.object({
363
+ kind: o.literal("result"),
364
+ nodeId: o.string().min(1),
365
+ /** Dot path into the result, e.g. `"state"`. */
366
+ path: o.string().optional()
367
+ }),
368
+ o.object({
369
+ kind: o.literal("tries"),
370
+ nodeId: o.string().min(1)
371
+ }),
372
+ /**
373
+ * How many times the learner has been *shown* a step, read off the history.
374
+ *
375
+ * Distinct from `tries`, which counts gradings: a content step is never
376
+ * graded, so a loop that goes round an explanation has nothing else to count.
377
+ * This is what gives a loop a way out — "after the third time, move on".
378
+ */
379
+ o.object({
380
+ kind: o.literal("visits"),
381
+ nodeId: o.string().min(1)
382
+ }),
383
+ /**
384
+ * How sure the learner said they were, `0`–`1`, when `meta.askConfidence` is
385
+ * on. Undefined for a step they were never asked about, which makes any
386
+ * comparison false rather than accidentally true.
387
+ *
388
+ * Confident and wrong is a different learner from unsure and wrong, and it is
389
+ * the branch worth drawing: one has a misconception, the other has a gap.
390
+ */
391
+ o.object({
392
+ kind: o.literal("confidence"),
393
+ nodeId: o.string().min(1)
394
+ }),
395
+ /**
396
+ * Seconds spent on one step, or on the whole attempt when `nodeId` is
397
+ * omitted. Time *spent*, on the same basis as the limits: the clock pauses
398
+ * when the learner is not there.
399
+ */
400
+ o.object({
401
+ kind: o.literal("timeSpent"),
402
+ nodeId: o.string().min(1).optional()
403
+ }),
404
+ /**
405
+ * Seconds left on `meta.timeLimit`. Undefined when the flow has no limit, so
406
+ * "less than a minute left" is simply false rather than true for everyone.
407
+ */
408
+ o.object({ kind: o.literal("timeRemaining") }),
409
+ /** Points earned so far, over the whole attempt or one scope. */
410
+ o.object({ kind: o.literal("score"), scope: O.optional() }),
411
+ /** Earned/possible so far, in `[0, 1]`. `0` when nothing is scorable yet. */
412
+ o.object({ kind: o.literal("scoreRatio"), scope: O.optional() }),
413
+ /**
414
+ * How many tasks so far ended in a given outcome — "at least three correct"
415
+ * being the branch a teacher reaches for most often.
416
+ *
417
+ * Counts tasks, not points: a partly-credited answer counts once, and only
418
+ * if it reached the state being counted. That is what "how many did they get
419
+ * right" means to the person writing the branch, whereas `score` is the
420
+ * measure to use when partial credit should carry weight.
421
+ */
422
+ o.object({
423
+ kind: o.literal("resultCount"),
424
+ state: oe.default("correct"),
425
+ scope: O.optional()
426
+ })
427
+ ]), Me = [
428
+ "eq",
429
+ "ne",
430
+ "gt",
431
+ "gte",
432
+ "lt",
433
+ "lte",
434
+ "in",
435
+ "notIn",
436
+ "isTrue"
437
+ ], Oe = o.enum(Me), z = o.union([o.string(), o.number(), o.boolean()]), N = o.lazy(
438
+ () => o.union([
439
+ o.object({ type: o.literal("always") }),
440
+ o.object({
441
+ type: o.literal("compare"),
442
+ left: Ee,
443
+ op: Oe,
444
+ right: o.union([z, o.array(z)]).optional()
445
+ }),
446
+ o.object({
447
+ type: o.literal("and"),
448
+ conditions: o.array(N)
449
+ }),
450
+ o.object({
451
+ type: o.literal("or"),
452
+ conditions: o.array(N)
453
+ }),
454
+ o.object({ type: o.literal("not"), condition: N })
455
+ ])
456
+ ), je = o.object({ x: o.number(), y: o.number() }), Le = o.object({
457
+ id: o.string().min(1),
458
+ type: o.string().min(1),
459
+ position: je,
460
+ data: o.record(o.string(), o.unknown()).default({}),
461
+ /**
462
+ * The pool this step belongs to, if any — see `BitflowMeta.pools`.
463
+ *
464
+ * Membership lives on the node rather than the pool holding a list of ids,
465
+ * so deleting a step cannot leave a pool pointing at something that is gone.
466
+ */
467
+ pool: o.string().min(1).optional(),
468
+ /**
469
+ * The section this step belongs to, if any — see `BitflowMeta.sections`.
470
+ *
471
+ * Orthogonal to `pool`: a pool decides *which* steps a learner gets and in
472
+ * what order, a section says what a run of steps have in common — the
473
+ * passage they are all about, the name the progress line shows, and the
474
+ * scope a condition can count over.
475
+ */
476
+ section: o.string().min(1).optional()
477
+ }), Pe = o.object({
478
+ id: o.string().min(1),
479
+ source: o.string().min(1),
480
+ target: o.string().min(1),
481
+ sourceHandle: o.string().optional(),
482
+ targetHandle: o.string().optional(),
483
+ /** Shown on the canvas; useful to label the two sides of a branch. */
484
+ label: o.string().optional(),
485
+ /** Absent means "always follow". */
486
+ condition: N.optional(),
487
+ /**
488
+ * What arriving over this edge clears on the step it lands on.
489
+ *
490
+ * This is what makes a remediation loop work. Without it, an edge that goes
491
+ * back to a question the learner got wrong lands on a step that already has
492
+ * a result, so the runtime shows the old answer marked wrong with no way to
493
+ * change it — the loop is drawable but does nothing.
494
+ *
495
+ * `"result"` clears the grading and keeps what they wrote, the same bargain
496
+ * Try again makes. `"answer"` clears both, which is what a task that
497
+ * *measures* something (a timed run, a typing speed) needs: its recorded
498
+ * figure has to be taken again, not edited.
499
+ *
500
+ * Absent means the step is left exactly as it was, which is right for an
501
+ * edge that goes back so the learner can *read* an answer again.
502
+ */
503
+ resetTarget: o.enum(["result", "answer"]).optional()
504
+ }), _e = o.object({
505
+ x: o.number(),
506
+ y: o.number(),
507
+ zoom: o.number().positive()
508
+ }), Ce = o.object({
509
+ /**
510
+ * Stable identity of the assessment. An attempt snapshot records it so a
511
+ * host cannot restore an attempt into a different flow.
512
+ */
513
+ id: o.string().min(1),
514
+ title: o.string().default(""),
515
+ description: o.string().optional(),
516
+ locale: Ae.default("en"),
517
+ /**
518
+ * Ask the learner how sure they are after each task. A whole-flow setting
519
+ * rather than a per-task one: being asked on some questions and not others
520
+ * reads as a hint about which ones are hard.
521
+ */
522
+ askConfidence: o.boolean().default(!1),
523
+ /** Ask the learner to explain their reasoning after each task. */
524
+ askReasoning: o.boolean().default(!1),
525
+ /**
526
+ * Groups of interchangeable steps, of which each learner gets a random few.
527
+ *
528
+ * The members stay ordinary nodes wired into the graph as usual; drawing
529
+ * only decides which of them a given attempt walks through, and the rest are
530
+ * stepped over as though they were not there. That keeps pools out of every
531
+ * bit, out of the condition language, and out of the report — which already
532
+ * copes with learners who saw different items, because branching does the
533
+ * same thing.
534
+ */
535
+ pools: o.array(
536
+ o.object({
537
+ id: o.string().min(1),
538
+ /** Shown to the author. Learners never see it. */
539
+ label: o.string().default(""),
540
+ /** How many members each learner gets. */
541
+ draw: o.number().int().positive(),
542
+ /**
543
+ * Show the drawn members in a random order rather than the order they
544
+ * are wired in.
545
+ *
546
+ * The draw was always recorded in the order it happened; until this
547
+ * existed nothing read that order back, so "the same ten questions,
548
+ * shuffled" — which is what most people mean by a randomised test —
549
+ * could not be expressed at all. A shuffled pool navigates by its drawn
550
+ * order instead of by its internal edges, so it must have exactly one
551
+ * way out; `validateFlow` says so when it does not. The way in needs no
552
+ * such rule — every edge into a shuffled pool lands on whichever member
553
+ * the draw put first.
554
+ */
555
+ shuffle: o.boolean().default(!1)
556
+ })
557
+ ).default([]),
558
+ /**
559
+ * Named runs of steps that belong together.
560
+ *
561
+ * A section carries the thing its steps share and the flow had nowhere to
562
+ * put: the passage, listing or diagram every question in it refers to. Before
563
+ * this, a reading comprehension with five questions meant pasting the passage
564
+ * into all five, because a content step shows its text once and is gone.
565
+ *
566
+ * It is also a scope — `ScopeSchema` — so "did they pass this section" is a
567
+ * branch rather than a hand-listed set of node ids, and the name shows on the
568
+ * progress line so a learner knows where they are in a long assessment.
569
+ */
570
+ sections: o.array(
571
+ o.object({
572
+ id: o.string().min(1),
573
+ /** Shown to the learner above every step in the section. */
574
+ label: o.string().default(""),
575
+ /**
576
+ * Markdown rendered above every step in the section — the passage, the
577
+ * code listing, the data table the questions are about.
578
+ */
579
+ markdown: o.string().default("")
580
+ })
581
+ ).default([]),
582
+ /**
583
+ * How freely the learner may move.
584
+ *
585
+ * - `linear` — forwards only. An exam.
586
+ * - `back` — they may step back through what they have seen. The default,
587
+ * and what the runtime always did.
588
+ * - `free` — they may jump to any step they have already visited, from a
589
+ * list. That list doubles as the check-your-work screen before finishing.
590
+ *
591
+ * Going back re-runs the branch on the way forward again, in every mode: an
592
+ * answer changed on the second pass has to be able to send them elsewhere.
593
+ */
594
+ navigation: o.enum(["linear", "back", "free"]).default("back"),
595
+ /**
596
+ * Whether a learner may pass on a task without answering. A task may
597
+ * override it — `EvaluationSchema.allowSkip` — for the one question everyone
598
+ * has to attempt.
599
+ */
600
+ allowSkip: o.boolean().default(!0),
601
+ /**
602
+ * Seconds for the whole assessment. Counted as time actually spent, not wall
603
+ * clock: closing the tab pauses it. That is both fairer and the only rule
604
+ * that survives a reload, since a snapshot records time per task rather than
605
+ * a deadline.
606
+ */
607
+ timeLimit: o.number().int().positive().optional()
608
+ }), De = o.object({
609
+ version: o.literal(Ne),
610
+ meta: Ce,
611
+ nodes: o.array(Le),
612
+ edges: o.array(Pe),
613
+ /** Editor camera. Presentation only; never affects a learner's run. */
614
+ viewport: _e.optional()
615
+ }), Fe = o.object({
616
+ message: o.string(),
617
+ severity: o.enum(["error", "warning", "info", "success"])
618
+ }), Re = o.object({
619
+ mode: o.enum(["auto", "skip"]).default("auto"),
620
+ enableRetry: o.boolean().default(!1),
621
+ showFeedback: o.boolean().default(!0),
622
+ /**
623
+ * What this task is worth relative to the others. The runtime multiplies the
624
+ * bit's own score by it, so a bit never has to think about weighting — and a
625
+ * new bit gets it without doing anything.
626
+ */
627
+ weight: o.number().min(0).default(1),
628
+ /**
629
+ * Seconds the learner gets on this task, counted from when they arrive.
630
+ * Omitted means no limit.
631
+ */
632
+ timeLimit: o.number().int().positive().optional(),
633
+ /**
634
+ * Whether this task may be passed on. Omitted follows the flow's
635
+ * `meta.allowSkip`, which is the answer for all but the odd question that
636
+ * everyone has to attempt.
637
+ */
638
+ allowSkip: o.boolean().optional()
639
+ }), yt = () => Re.parse({}), Be = o.object({
640
+ state: oe,
641
+ /** Omitted means "worth one point, earned iff correct". */
642
+ score: o.object({ earned: o.number(), possible: o.number().nonnegative() }).optional(),
643
+ feedback: o.array(Fe).optional(),
644
+ /** Whether the learner may try again, when the bit allows retries at all. */
645
+ allowRetry: o.boolean().optional(),
646
+ /** Bit-specific extras, e.g. which choices were right. */
647
+ detail: o.record(o.string(), o.unknown()).optional()
648
+ }), re = 1, Ve = o.object({
649
+ /** `0`–`1`; how sure the learner said they were. */
650
+ level: o.number().min(0).max(1)
651
+ }), We = o.enum([
652
+ "inProgress",
653
+ "completed",
654
+ "abandoned"
655
+ ]), Ue = o.object({
656
+ schemaVersion: o.literal(re),
657
+ flowId: o.string().min(1),
658
+ flowSchemaVersion: o.number().int(),
659
+ attemptId: o.string().min(1),
660
+ status: We,
661
+ currentNodeId: o.string().min(1),
662
+ /** Visited node ids, oldest first, including the current one. */
663
+ history: o.array(o.string().min(1)),
664
+ answers: o.record(o.string(), o.unknown()),
665
+ results: o.record(o.string(), Be),
666
+ tries: o.record(o.string(), o.number().int().nonnegative()),
667
+ elapsedMs: o.record(o.string(), o.number().nonnegative()),
668
+ /**
669
+ * Pool id → the node ids this attempt drew, in the order they were drawn.
670
+ *
671
+ * Recorded rather than re-rolled, so a reload resumes the same assessment.
672
+ * A pool the document has gained since is drawn on first use; one it has
673
+ * lost is simply never consulted again.
674
+ */
675
+ pools: o.record(o.string(), o.array(o.string())).default({}),
676
+ confidence: o.record(o.string(), Ve).optional(),
677
+ reasoning: o.record(o.string(), o.string()).optional(),
678
+ startedAt: o.iso.datetime(),
679
+ updatedAt: o.iso.datetime(),
680
+ /** Timestamp the current node was entered; drives `elapsedMs`. */
681
+ enteredAt: o.iso.datetime(),
682
+ completedAt: o.iso.datetime().optional()
683
+ }), S = (e) => (e ?? /* @__PURE__ */ new Date()).toISOString(), y = (e, t, n) => ({ ...e, ...t, updatedAt: S(n) }), He = (e, t = {}) => {
684
+ const n = fe(e);
685
+ if (!n)
686
+ return {
687
+ ok: !1,
688
+ error: w("INVALID_FLOW", "The flow has no nodes to start at.")
689
+ };
690
+ const s = S(t.now);
691
+ return {
692
+ ok: !0,
693
+ value: {
694
+ schemaVersion: re,
695
+ flowId: e.meta.id,
696
+ flowSchemaVersion: e.version,
697
+ attemptId: t.attemptId ?? Te("attempt"),
698
+ status: "inProgress",
699
+ currentNodeId: n,
700
+ history: [n],
701
+ answers: {},
702
+ results: {},
703
+ tries: {},
704
+ elapsedMs: {},
705
+ // Drawn once, at the start, and carried in the snapshot from then on:
706
+ // re-rolling on every render would hand the learner a different
707
+ // assessment each time the page reloaded.
708
+ pools: me(e, t.random),
709
+ startedAt: s,
710
+ updatedAt: s,
711
+ enteredAt: s
712
+ }
713
+ };
714
+ }, vt = (e, t) => {
715
+ const n = typeof t == "string" ? ze(t) : t;
716
+ if (n === void 0)
717
+ return {
718
+ ok: !1,
719
+ error: w("INVALID_ATTEMPT", "The attempt is not valid JSON.")
720
+ };
721
+ const s = Ue.safeParse(n);
722
+ if (!s.success)
723
+ return {
724
+ ok: !1,
725
+ error: w(
726
+ "INVALID_ATTEMPT",
727
+ "The attempt does not match the attempt snapshot schema.",
728
+ _(s.error.issues)
729
+ )
730
+ };
731
+ const r = s.data;
732
+ return r.flowId !== e.meta.id ? {
733
+ ok: !1,
734
+ error: w(
735
+ "FLOW_ATTEMPT_MISMATCH",
736
+ `The attempt belongs to flow "${r.flowId}" but the loaded flow is "${e.meta.id}".`
737
+ )
738
+ } : r.flowSchemaVersion !== e.version ? {
739
+ ok: !1,
740
+ error: w(
741
+ "FLOW_ATTEMPT_MISMATCH",
742
+ `The attempt was recorded against flow schema version ${r.flowSchemaVersion}, but the flow is version ${e.version}.`
743
+ )
744
+ } : p(e, r.currentNodeId) ? { ok: !0, value: r } : {
745
+ ok: !1,
746
+ error: w(
747
+ "FLOW_ATTEMPT_MISMATCH",
748
+ `The attempt stops at node "${r.currentNodeId}", which the flow does not contain.`
749
+ )
750
+ };
751
+ }, ze = (e) => {
752
+ try {
753
+ return JSON.parse(e);
754
+ } catch {
755
+ return;
756
+ }
757
+ }, kt = (e, t) => y(e, { status: "abandoned" }, t), It = (e, t, n, s) => y(e, { answers: { ...e.answers, [t]: n } }, s), St = (e, t, n, s) => y(
758
+ e,
759
+ { confidence: { ...e.confidence, [t]: n } },
760
+ s
761
+ ), Tt = (e, t, n, s) => y(
762
+ e,
763
+ { reasoning: { ...e.reasoning, [t]: n } },
764
+ s
765
+ ), $t = (e, t, n = {}) => {
766
+ if (!p(e, t))
767
+ return {
768
+ ok: !1,
769
+ error: w(
770
+ "INVALID_FLOW",
771
+ `The flow does not contain a node "${t}".`
772
+ )
773
+ };
774
+ const s = He(e, n);
775
+ return s.ok ? {
776
+ ok: !0,
777
+ value: { ...s.value, currentNodeId: t, history: [t] }
778
+ } : s;
779
+ }, Nt = async (e, t, n, s, r) => {
780
+ const i = p(e, n);
781
+ if (!i)
782
+ return {
783
+ ok: !1,
784
+ error: w(
785
+ "INVALID_FLOW",
786
+ `The flow does not contain a node "${n}".`
787
+ )
788
+ };
789
+ const a = b(i.type);
790
+ if (!a)
791
+ return {
792
+ ok: !1,
793
+ error: w(
794
+ "UNKNOWN_BIT_TYPE",
795
+ `No bit is registered for type "${i.type}".`
796
+ )
797
+ };
798
+ const u = s === void 0 ? t.answers[n] : s;
799
+ let f;
800
+ if (!a.evaluate)
801
+ f = { state: "unknown" };
802
+ else {
803
+ const d = a.schema.safeParse(i.data);
804
+ if (!d.success)
805
+ return {
806
+ ok: !1,
807
+ error: w(
808
+ "INVALID_FLOW",
809
+ `Node "${n}" (${i.type}) is not configured correctly: ${d.error.issues.map((h) => h.message).join(" ")}`
810
+ )
811
+ };
812
+ try {
813
+ f = await a.evaluate({ data: d.data, answer: u });
814
+ } catch (h) {
815
+ return {
816
+ ok: !1,
817
+ error: w(
818
+ "EVALUATION_FAILED",
819
+ `Evaluating node "${n}" (${i.type}) failed: ${h instanceof Error ? h.message : String(h)}`
820
+ )
821
+ };
822
+ }
823
+ }
824
+ return {
825
+ ok: !0,
826
+ value: y(
827
+ t,
828
+ {
829
+ answers: { ...t.answers, [n]: u },
830
+ results: { ...t.results, [n]: Je(f, i) },
831
+ tries: { ...t.tries, [n]: (t.tries[n] ?? 0) + 1 }
832
+ },
833
+ r
834
+ )
835
+ };
836
+ }, Je = (e, t) => {
837
+ const s = t.data?.evaluation?.weight;
838
+ if (typeof s != "number" || s === 1 || s < 0) return e;
839
+ const r = Y(e);
840
+ return {
841
+ ...e,
842
+ score: { earned: r.earned * s, possible: r.possible * s }
843
+ };
844
+ }, At = (e, t, n) => y(
845
+ e,
846
+ {
847
+ results: { ...e.results, [t]: { state: "unknown" } },
848
+ tries: { ...e.tries, [t]: (e.tries[t] ?? 0) + 1 }
849
+ },
850
+ n
851
+ ), Et = (e, t, n) => {
852
+ const s = { ...e.results };
853
+ return delete s[t], y(e, { results: s }, n);
854
+ }, A = (e, t) => {
855
+ const n = Math.max(
856
+ 0,
857
+ (t ?? /* @__PURE__ */ new Date()).getTime() - new Date(e.enteredAt).getTime()
858
+ );
859
+ return {
860
+ elapsedMs: {
861
+ ...e.elapsedMs,
862
+ [e.currentNodeId]: (e.elapsedMs[e.currentNodeId] ?? 0) + n
863
+ }
864
+ };
865
+ }, Mt = (e, t, n) => {
866
+ if (t.status !== "inProgress") return t;
867
+ const s = S(n), r = x(e, t.currentNodeId, ve(e, t, n), {
868
+ // Steps this attempt did not draw are walked past, not stopped on.
869
+ isActive: (u) => K(t, u),
870
+ // A shuffled pool is walked in the order this attempt drew, not as wired.
871
+ order: t.pools
872
+ });
873
+ if (r === null)
874
+ return y(
875
+ t,
876
+ {
877
+ ...A(t, n),
878
+ status: "completed",
879
+ completedAt: s
880
+ },
881
+ n
882
+ );
883
+ const i = r.nodeId, a = b(p(e, i)?.type ?? "")?.kind === "end";
884
+ return y(
885
+ t,
886
+ {
887
+ ...A(t, n),
888
+ // Clearing before arriving, not after, so the learner never sees the
889
+ // stale result flash on the step they were sent back to.
890
+ ...Ye(t, i, r.edge?.resetTarget),
891
+ currentNodeId: i,
892
+ history: [...t.history, i],
893
+ enteredAt: s,
894
+ ...a ? { status: "completed", completedAt: s } : {}
895
+ },
896
+ n
897
+ );
898
+ }, Ye = (e, t, n) => {
899
+ if (!n) return {};
900
+ const s = { ...e.results };
901
+ if (delete s[t], n === "result") return { results: s };
902
+ const r = { ...e.answers };
903
+ return delete r[t], { results: s, answers: r };
904
+ }, Ot = (e, t, n) => {
905
+ if (!qe(e, t)) return t;
906
+ const s = ee(t);
907
+ return s === null || !p(e, s) ? t : y(
908
+ t,
909
+ {
910
+ ...A(t, n),
911
+ currentNodeId: s,
912
+ history: t.history.slice(0, -1),
913
+ enteredAt: S(n),
914
+ status: "inProgress",
915
+ completedAt: void 0
916
+ },
917
+ n
918
+ );
919
+ }, qe = (e, t) => {
920
+ if (e.meta.navigation === "linear") return !1;
921
+ const n = ee(t);
922
+ return n !== null && p(e, n) !== void 0;
923
+ }, jt = (e, t, n, s) => {
924
+ if (!we(e, t, n) || n === t.currentNodeId) return t;
925
+ const r = t.history.indexOf(n);
926
+ return y(
927
+ t,
928
+ {
929
+ ...A(t, s),
930
+ currentNodeId: n,
931
+ history: t.history.slice(0, r + 1),
932
+ enteredAt: S(s),
933
+ status: "inProgress",
934
+ completedAt: void 0
935
+ },
936
+ s
937
+ );
938
+ }, Lt = (e) => e.status === "completed", Pt = (e, t) => Q(e, t.currentNodeId), _t = o.object({
939
+ /**
940
+ * A `data:` URI. A plain URL still parses, because documents written before
941
+ * this existed have to keep working, but the editor no longer produces one
942
+ * and warns where it finds one.
943
+ */
944
+ src: o.string().default(""),
945
+ /**
946
+ * What the picture shows. Required wherever the picture carries meaning —
947
+ * each task decides that for itself, because a decorative image is a real
948
+ * thing too.
949
+ */
950
+ alt: o.string().default("")
951
+ }), Ct = (e) => e.startsWith("data:"), Dt = (e) => e.length, Ft = (e) => e < 1024 ? `${e} B` : e < 1024 * 1024 ? `${Math.round(e / 1024)} kB` : `${(e / (1024 * 1024)).toFixed(1)} MB`, Rt = (e, t, n) => {
952
+ const s = Math.max(e, t);
953
+ if (s <= n || s === 0)
954
+ return { width: Math.round(e), height: Math.round(t) };
955
+ const r = n / s;
956
+ return {
957
+ width: Math.max(1, Math.round(e * r)),
958
+ height: Math.max(1, Math.round(t * r))
959
+ };
960
+ }, Ge = "en", J = (e) => typeof e == "string" && se.includes(e), D = (e, t = Ge) => {
961
+ if (!e) return t;
962
+ if (J(e)) return e;
963
+ const n = e.split("-")[0]?.toLowerCase();
964
+ return J(n) ? n : t;
965
+ }, Ke = (e, t) => t ? e.replace(
966
+ /\{(\w+)\}/g,
967
+ (n, s) => s in t ? String(t[s]) : n
968
+ ) : e, Qe = (e, t, n, s) => {
969
+ const r = D(typeof n == "string" ? n : void 0), i = e[r]?.[t] ?? e.en[t] ?? t;
970
+ return Ke(i, s);
971
+ }, Bt = (e, t) => {
972
+ const n = D(typeof t == "string" ? t : void 0);
973
+ return (s, r) => Qe(e, s, n, r);
974
+ }, Xe = (e, t, n = { dateStyle: "medium" }) => new Intl.DateTimeFormat(D(
975
+ typeof t == "string" ? t : void 0
976
+ ), n).format(new Date(e)), Vt = (e, t) => Xe(e, t, { dateStyle: "medium", timeStyle: "short" }), Wt = (e) => {
977
+ const t = Math.max(0, Math.round(e / 1e3)), n = t % 60, s = Math.floor(t / 60) % 60, r = Math.floor(t / 3600), i = (a) => String(a).padStart(2, "0");
978
+ return r > 0 ? `${r}:${i(s)}:${i(n)}` : `${s}:${i(n)}`;
979
+ }, Ze = (e, t, n) => {
980
+ if (!t.condition) return [];
981
+ const s = [], r = new Set(
982
+ e.nodes.filter((l) => b(l.type)?.kind === "task").map((l) => l.id)
983
+ ), i = new Set(e.nodes.map((l) => l.id)), a = new Set(e.meta.sections.map((l) => l.id)), u = ie(e, t.source), f = u.filter((l) => r.has(l)).length, d = new Set(u), h = (l) => {
984
+ if (!l) return f;
985
+ switch (l.kind) {
986
+ case "nodes":
987
+ return l.nodeIds.filter(
988
+ (c) => r.has(c) && d.has(c)
989
+ ).length;
990
+ case "section":
991
+ return e.nodes.filter(
992
+ (c) => c.section === l.id && r.has(c.id) && d.has(c.id)
993
+ ).length;
994
+ case "last":
995
+ return Math.min(l.count, f);
996
+ }
997
+ }, T = (l) => {
998
+ if (l) {
999
+ if (l.kind === "section") {
1000
+ a.has(l.id) ? h(l) === 0 && s.push({
1001
+ path: n,
1002
+ message: `Section "${l.id}" has no task before this connection, so counting it here is never anything but zero.`
1003
+ }) : s.push({
1004
+ path: n,
1005
+ message: `This rule counts section "${l.id}", which the flow does not declare.`
1006
+ });
1007
+ return;
1008
+ }
1009
+ if (l.kind === "nodes") {
1010
+ if (l.nodeIds.length === 0) {
1011
+ s.push({
1012
+ path: n,
1013
+ message: "This rule counts no steps at all, so it is always zero."
1014
+ });
1015
+ return;
1016
+ }
1017
+ for (const c of l.nodeIds)
1018
+ i.has(c) ? r.has(c) ? d.has(c) || s.push({
1019
+ path: n,
1020
+ message: `The learner cannot have reached "${c}" by this point in the flow, so counting it here does nothing.`
1021
+ }) : s.push({
1022
+ path: n,
1023
+ message: `"${c}" is not a task, so it never contributes a result to count.`
1024
+ }) : s.push({
1025
+ path: n,
1026
+ message: `This rule counts "${c}", which is not a node in this flow.`
1027
+ });
1028
+ }
1029
+ }
1030
+ }, E = (l) => {
1031
+ switch (l.type) {
1032
+ case "always":
1033
+ return;
1034
+ case "not":
1035
+ E(l.condition);
1036
+ return;
1037
+ case "and":
1038
+ case "or":
1039
+ l.conditions.length === 0 && s.push({
1040
+ path: n,
1041
+ message: l.type === "and" ? "This condition has nothing in it, so it is always true. Add a rule or remove it." : "This condition has nothing in it, so it is never true. Add a rule or remove it."
1042
+ }), l.conditions.forEach(E);
1043
+ return;
1044
+ case "compare":
1045
+ ae(l);
1046
+ }
1047
+ }, ae = (l) => {
1048
+ const { left: c, op: g, right: m } = l;
1049
+ "scope" in c && T(c.scope);
1050
+ const v = "nodeId" in c ? c.nodeId : void 0;
1051
+ if (v !== void 0 && i.has(v) && (!(c.kind === "answer" || c.kind === "visits" || c.kind === "timeSpent") && !r.has(v) ? s.push({
1052
+ path: n,
1053
+ message: `"${v}" is not a task, so it never has a result to compare. Point this at a task.`
1054
+ }) : d.has(v) || s.push({
1055
+ path: n,
1056
+ message: `The learner cannot have reached "${v}" by this point in the flow, so this connection is never followed.`
1057
+ })), c.kind === "confidence" && !e.meta.askConfidence && s.push({
1058
+ path: n,
1059
+ message: "This flow does not ask how sure the learner is, so there is never a confidence to compare. Turn on the confidence question, or use a different rule."
1060
+ }), c.kind === "timeRemaining" && e.meta.timeLimit === void 0 && s.push({
1061
+ path: n,
1062
+ message: "This flow has no time limit, so there is never any time remaining to compare. Set a limit for the whole assessment, or use time spent instead."
1063
+ }), g === "in" || g === "notIn") {
1064
+ Array.isArray(m) ? m.length === 0 && s.push({
1065
+ path: n,
1066
+ message: "The list to check against is empty, so this is never true."
1067
+ }) : s.push({
1068
+ path: n,
1069
+ message: "This comparison needs a list of values to check against."
1070
+ });
1071
+ return;
1072
+ }
1073
+ if (g !== "isTrue") {
1074
+ if (m === void 0) {
1075
+ s.push({ path: n, message: "This comparison has nothing to compare against." });
1076
+ return;
1077
+ }
1078
+ if (g === "gt" || g === "gte" || g === "lt" || g === "lte") {
1079
+ if (typeof m != "number") {
1080
+ s.push({
1081
+ path: n,
1082
+ message: `"${String(m)}" is not a number, and this comparison only works on numbers.`
1083
+ });
1084
+ return;
1085
+ }
1086
+ B(c, g, m), F(c, m), R(c, m);
1087
+ return;
1088
+ }
1089
+ c.kind === "result" && c.path === "state" && typeof m == "string" && !P.includes(m) && s.push({
1090
+ path: n,
1091
+ message: `"${m}" is not a possible outcome. Use one of: ${P.join(", ")}.`
1092
+ }), typeof m == "number" && (B(c, g, m), F(c, m), R(c, m));
1093
+ }
1094
+ }, F = (l, c) => {
1095
+ l.kind === "confidence" && (c >= 0 && c <= 1 || s.push({
1096
+ path: n,
1097
+ message: `How sure the learner is runs from 0 to 1, so "${c}" is outside it. The top of the five-point scale is 1, the middle is 0.6.`
1098
+ }));
1099
+ }, R = (l, c) => {
1100
+ l.kind === "scoreRatio" && (c >= 0 && c <= 1 || s.push({
1101
+ path: n,
1102
+ message: `The share of the marks runs from 0 to 1, so "${c}" is outside it. Half the marks is 0.5, all of them is 1.`
1103
+ }));
1104
+ }, B = (l, c, g) => {
1105
+ if (l.kind !== "resultCount") return;
1106
+ if (g < 0 || !Number.isInteger(g)) {
1107
+ s.push({
1108
+ path: n,
1109
+ message: "The number of tasks has to be a whole number, zero or more."
1110
+ });
1111
+ return;
1112
+ }
1113
+ const m = h(l.scope);
1114
+ (c === "gte" || c === "eq") && g > m && s.push({
1115
+ path: n,
1116
+ message: m === 0 ? "There are no tasks before this connection, so counting answers here is never true." : `Only ${m} task(s) come before this connection, so "${g}" can never be reached.`
1117
+ }), c === "gt" && g >= m && s.push({
1118
+ path: n,
1119
+ message: `Only ${m} task(s) come before this connection, so more than ${g} can never be reached.`
1120
+ });
1121
+ };
1122
+ return E(t.condition), s;
1123
+ }, ie = (e, t) => {
1124
+ const n = /* @__PURE__ */ new Set([t]), s = [t];
1125
+ for (; s.length > 0; ) {
1126
+ const r = s.shift();
1127
+ for (const i of C(e, r))
1128
+ n.has(i.source) || (n.add(i.source), s.push(i.source));
1129
+ }
1130
+ return [...n];
1131
+ }, Ut = (e, t) => ie(e, t).filter((n) => {
1132
+ const s = e.nodes.find((r) => r.id === n);
1133
+ return s && b(s.type)?.kind === "task";
1134
+ }), Ht = (e) => {
1135
+ let t = e;
1136
+ if (typeof t == "string")
1137
+ try {
1138
+ t = JSON.parse(t);
1139
+ } catch (s) {
1140
+ return {
1141
+ ok: !1,
1142
+ error: w(
1143
+ "INVALID_FLOW",
1144
+ `The flow is not valid JSON: ${s instanceof Error ? s.message : String(s)}`
1145
+ )
1146
+ };
1147
+ }
1148
+ const n = De.safeParse(t);
1149
+ return n.success ? { ok: !0, value: n.data } : {
1150
+ ok: !1,
1151
+ error: w(
1152
+ "INVALID_FLOW",
1153
+ "The flow does not match the .bitflow schema.",
1154
+ _(n.error.issues)
1155
+ )
1156
+ };
1157
+ }, zt = (e) => {
1158
+ const t = [], n = /* @__PURE__ */ new Set(), s = /* @__PURE__ */ new Set();
1159
+ return e.nodes.forEach((r, i) => {
1160
+ n.has(r.id) && t.push({
1161
+ path: `nodes.${i}.id`,
1162
+ message: `Duplicate node id "${r.id}".`
1163
+ }), n.add(r.id);
1164
+ const a = b(r.type);
1165
+ if (!a) {
1166
+ nt() && t.push({
1167
+ path: `nodes.${i}.type`,
1168
+ message: `Unknown bit type "${r.type}".`
1169
+ });
1170
+ return;
1171
+ }
1172
+ const u = a.schema.safeParse(r.data);
1173
+ if (!u.success)
1174
+ for (const f of _(u.error.issues))
1175
+ t.push({
1176
+ path: `nodes.${i}.data${f.path ? `.${f.path}` : ""}`,
1177
+ message: f.message
1178
+ });
1179
+ }), e.edges.forEach((r, i) => {
1180
+ if (s.has(r.id) && t.push({
1181
+ path: `edges.${i}.id`,
1182
+ message: `Duplicate edge id "${r.id}".`
1183
+ }), s.add(r.id), n.has(r.source) || t.push({
1184
+ path: `edges.${i}.source`,
1185
+ message: `Edge starts at "${r.source}", which is not a node in this flow.`
1186
+ }), n.has(r.target) || t.push({
1187
+ path: `edges.${i}.target`,
1188
+ message: `Edge ends at "${r.target}", which is not a node in this flow.`
1189
+ }), r.condition) {
1190
+ for (const a of L(r.condition))
1191
+ n.has(a) || t.push({
1192
+ path: `edges.${i}.condition`,
1193
+ message: `The condition refers to node "${a}", which is not a node in this flow.`
1194
+ });
1195
+ t.push(
1196
+ ...Ze(e, r, `edges.${i}.condition`)
1197
+ );
1198
+ }
1199
+ }), t.push(...tt(e)), t.push(...et(e)), t.push(...xe(e)), { valid: t.length === 0, diagnostics: t };
1200
+ }, xe = (e) => {
1201
+ const t = [], n = /* @__PURE__ */ new Set();
1202
+ return e.meta.sections.forEach((s, r) => {
1203
+ n.has(s.id) && t.push({
1204
+ path: `meta.sections.${r}.id`,
1205
+ message: `Duplicate section "${s.id}".`
1206
+ }), n.add(s.id), ye(e, s.id).length === 0 && t.push({
1207
+ path: `meta.sections.${r}.id`,
1208
+ message: `The section "${s.label || s.id}" has no steps in it, so nothing shows its text.`
1209
+ });
1210
+ }), e.nodes.forEach((s, r) => {
1211
+ s.section && !n.has(s.section) && t.push({
1212
+ path: `nodes.${r}.section`,
1213
+ message: `This step is in section "${s.section}", which the flow does not declare.`
1214
+ });
1215
+ const i = b(s.type)?.kind;
1216
+ s.section && (i === "start" || i === "end") && t.push({
1217
+ path: `nodes.${r}.section`,
1218
+ message: `A ${i} step cannot be part of a section.`
1219
+ });
1220
+ }), t;
1221
+ }, et = (e) => {
1222
+ const t = [], n = new Set(e.meta.pools.map((r) => r.id)), s = /* @__PURE__ */ new Set();
1223
+ return e.meta.pools.forEach((r, i) => {
1224
+ s.has(r.id) && t.push({
1225
+ path: `meta.pools.${i}.id`,
1226
+ message: `Duplicate pool "${r.id}".`
1227
+ }), s.add(r.id);
1228
+ const a = G(e, r.id);
1229
+ if (a.length === 0) {
1230
+ t.push({
1231
+ path: `meta.pools.${i}.id`,
1232
+ message: `The pool "${r.label || r.id}" has no steps in it.`
1233
+ });
1234
+ return;
1235
+ }
1236
+ if (r.draw > a.length && t.push({
1237
+ path: `meta.pools.${i}.draw`,
1238
+ message: `The pool "${r.label || r.id}" draws ${r.draw} steps but only has ${a.length}. Every learner would get all of them.`
1239
+ }), r.draw === a.length && a.length > 1 && !r.shuffle && t.push({
1240
+ path: `meta.pools.${i}.draw`,
1241
+ message: `The pool "${r.label || r.id}" draws all ${a.length} of its steps, so every learner gets the same ones. Turn on shuffle to vary the order instead.`
1242
+ }), !r.shuffle) return;
1243
+ const u = Z(e, r.id);
1244
+ if (u.length === 0)
1245
+ t.push({
1246
+ path: `meta.pools.${i}.shuffle`,
1247
+ message: `Nothing leads out of the shuffled pool "${r.label || r.id}", so a learner who finishes it has nowhere to go. Connect one of its steps to what comes next.`
1248
+ });
1249
+ else {
1250
+ const f = new Set(u.map((d) => d.source));
1251
+ f.size > 1 && t.push({
1252
+ path: `meta.pools.${i}.shuffle`,
1253
+ message: `${f.size} steps in the shuffled pool "${r.label || r.id}" lead out of it. Because the order changes for every learner, they would all be tried in turn rather than the one you drew. Leave the pool from a single step.`
1254
+ });
1255
+ }
1256
+ }), e.nodes.forEach((r, i) => {
1257
+ r.pool && !n.has(r.pool) && t.push({
1258
+ path: `nodes.${i}.pool`,
1259
+ message: `This step is in pool "${r.pool}", which the flow does not declare. It will be shown to everyone.`
1260
+ });
1261
+ const a = b(r.type)?.kind;
1262
+ r.pool && (a === "start" || a === "end") && t.push({
1263
+ path: `nodes.${i}.pool`,
1264
+ message: `A ${a} step cannot be part of a pool.`
1265
+ });
1266
+ }), t;
1267
+ }, tt = (e) => {
1268
+ const t = [];
1269
+ if (e.nodes.length === 0)
1270
+ return [{ path: "nodes", message: "The flow has no nodes." }];
1271
+ const n = e.nodes.filter((i) => C(e, i.id).length === 0);
1272
+ n.length === 0 ? t.push({
1273
+ path: "nodes",
1274
+ message: "Every node has an incoming edge, so the flow has no place to start."
1275
+ }) : n.length > 1 && t.push({
1276
+ path: "nodes",
1277
+ message: `The flow has ${n.length} possible starting points (${n.map((i) => `"${i.id}"`).join(", ")}). Connect them so only one node starts the flow.`
1278
+ });
1279
+ const s = /* @__PURE__ */ new Set(), r = n.map((i) => i.id);
1280
+ for (; r.length > 0; ) {
1281
+ const i = r.shift();
1282
+ if (!s.has(i)) {
1283
+ s.add(i);
1284
+ for (const a of k(e, i)) r.push(a.target);
1285
+ }
1286
+ }
1287
+ return e.nodes.forEach((i, a) => {
1288
+ if (!s.has(i.id)) {
1289
+ t.push({
1290
+ path: `nodes.${a}.id`,
1291
+ message: `Node "${i.id}" cannot be reached from the start of the flow.`
1292
+ });
1293
+ return;
1294
+ }
1295
+ const u = b(i.type);
1296
+ u && u.kind !== "end" && k(e, i.id).length === 0 && t.push({
1297
+ path: `nodes.${a}.id`,
1298
+ message: `Node "${i.id}" has no outgoing edge, so the learner has nowhere to go after it. Connect it to the next node or to an end.`
1299
+ });
1300
+ }), t;
1301
+ }, nt = () => de().length > 0, Jt = (e, t) => {
1302
+ if (typeof document > "u" || document.getElementById(e)) return;
1303
+ const n = document.createElement("style");
1304
+ n.id = e, n.textContent = t, document.head.append(n);
1305
+ };
1306
+ export {
1307
+ re as ATTEMPT_SCHEMA_VERSION,
1308
+ Ue as AttemptSnapshotSchema,
1309
+ We as AttemptStatusSchema,
1310
+ rt as BITFLOW_ERROR_CODES,
1311
+ it as BIT_KINDS,
1312
+ P as BIT_RESULT_STATES,
1313
+ Pe as BitEdgeSchema,
1314
+ Le as BitNodeSchema,
1315
+ Be as BitResultSchema,
1316
+ oe as BitResultStateSchema,
1317
+ De as BitflowDocumentSchema,
1318
+ Ce as BitflowMetaSchema,
1319
+ Me as COMPARE_OPS,
1320
+ Oe as CompareOpSchema,
1321
+ N as ConditionSchema,
1322
+ Ve as ConfidenceSchema,
1323
+ Ge as DEFAULT_LOCALE,
1324
+ Re as EvaluationSchema,
1325
+ Ne as FLOW_SCHEMA_VERSION,
1326
+ Fe as FeedbackMessageSchema,
1327
+ _t as ImageSchema,
1328
+ se as LOCALES,
1329
+ Ae as LocaleSchema,
1330
+ je as PositionSchema,
1331
+ O as ScopeSchema,
1332
+ Ee as ValueRefSchema,
1333
+ _e as ViewportSchema,
1334
+ kt as abandonAttempt,
1335
+ $t as attemptAt,
1336
+ w as bitflowError,
1337
+ qe as canGoPrevious,
1338
+ we as canGoTo,
1339
+ mt as canLeaveNode,
1340
+ ft as canSkip,
1341
+ lt as clearBits,
1342
+ pt as collectAnswers,
1343
+ gt as collectResults,
1344
+ ht as computeScore,
1345
+ ve as conditionContext,
1346
+ L as conditionNodeIds,
1347
+ U as conditionSectionIds,
1348
+ He as createAttempt,
1349
+ Te as createId,
1350
+ Bt as createTranslator,
1351
+ yt as defaultEvaluation,
1352
+ ke as distanceToEnd,
1353
+ me as drawPools,
1354
+ $ as evaluateCondition,
1355
+ Nt as evaluateNode,
1356
+ bt as flowProgress,
1357
+ Ft as formatBytes,
1358
+ Xe as formatDate,
1359
+ Vt as formatDateTime,
1360
+ Wt as formatDuration,
1361
+ b as getBit,
1362
+ p as getNode,
1363
+ V as getPath,
1364
+ Mt as goNext,
1365
+ Ot as goPrevious,
1366
+ jt as goTo,
1367
+ ct as hasBit,
1368
+ C as incomingEdges,
1369
+ Jt as injectStyles,
1370
+ Ke as interpolate,
1371
+ K as isActiveNode,
1372
+ Pt as isAtEnd,
1373
+ Lt as isComplete,
1374
+ Ct as isEmbedded,
1375
+ J as isLocale,
1376
+ Q as isTerminalNode,
1377
+ de as listBits,
1378
+ ut as nextNodeId,
1379
+ x as nextStep,
1380
+ k as outgoingEdges,
1381
+ Ht as parseFlow,
1382
+ Z as poolExitEdges,
1383
+ G as poolMembers,
1384
+ ee as previousNodeId,
1385
+ at as registerBit,
1386
+ D as resolveLocale,
1387
+ le as resolveValueRef,
1388
+ vt as restoreAttempt,
1389
+ Et as retryNode,
1390
+ Rt as scaledSize,
1391
+ Y as scoreOf,
1392
+ ye as sectionMembers,
1393
+ be as sectionOf,
1394
+ It as setAnswer,
1395
+ St as setConfidence,
1396
+ Tt as setReasoning,
1397
+ At as skipNode,
1398
+ Dt as sourceBytes,
1399
+ fe as startNodeId,
1400
+ wt as taskTimeLimit,
1401
+ Ut as tasksBefore,
1402
+ Se as timeSpent,
1403
+ Ie as timeSpentOn,
1404
+ _ as toDiagnostics,
1405
+ j as totalScore,
1406
+ Qe as translate,
1407
+ Ze as validateCondition,
1408
+ zt as validateFlow,
1409
+ dt as visitedSteps
1410
+ };