@mathscan/geometry-engine 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -7
- package/dist/core.cjs +1 -1
- package/dist/core.mjs +4 -4
- package/dist/entries/core.d.cts +1 -1
- package/dist/entries/core.d.ts +1 -1
- package/dist/entries/core.d.ts.map +1 -1
- package/dist/exercise.schema-Bma3qN9P.js +251 -0
- package/dist/exercise.schema-Bma3qN9P.js.map +1 -0
- package/dist/exercise.schema-Oj5ZH-yO.cjs +2 -0
- package/dist/exercise.schema-Oj5ZH-yO.cjs.map +1 -0
- package/dist/exerciseLoad-Db9Cigxi.cjs +2 -0
- package/dist/exerciseLoad-Db9Cigxi.cjs.map +1 -0
- package/dist/{geometryAttemptStatus-5L_q6-9l.js → exerciseLoad-DgeDXYbf.js} +84 -71
- package/dist/exerciseLoad-DgeDXYbf.js.map +1 -0
- package/dist/geometry/entities/angle.d.cts +56 -0
- package/dist/geometry/entities/angle.d.ts +57 -0
- package/dist/geometry/entities/angle.d.ts.map +1 -0
- package/dist/geometry/entities/circle.d.ts.map +1 -1
- package/dist/geometry/entities/index.d.cts +8 -4
- package/dist/geometry/entities/index.d.ts +8 -4
- package/dist/geometry/entities/index.d.ts.map +1 -1
- package/dist/geometry/entities/line.d.ts.map +1 -1
- package/dist/geometry/entities/point.d.ts.map +1 -1
- package/dist/geometry/entities/relations.d.cts +8 -1
- package/dist/geometry/entities/relations.d.ts +8 -1
- package/dist/geometry/entities/relations.d.ts.map +1 -1
- package/dist/geometry/entities/segment.d.ts.map +1 -1
- package/dist/geometry/entityModel.d.cts +21 -3
- package/dist/geometry/entityModel.d.ts +21 -3
- package/dist/geometry/entityModel.d.ts.map +1 -1
- package/dist/geometryState.schema-B5ccMKDa.cjs +2 -0
- package/dist/geometryState.schema-B5ccMKDa.cjs.map +1 -0
- package/dist/geometryState.schema-CKFPlX8j.js +1177 -0
- package/dist/geometryState.schema-CKFPlX8j.js.map +1 -0
- package/dist/goals-BlSeniyt.js +563 -0
- package/dist/goals-BlSeniyt.js.map +1 -0
- package/dist/goals-DLbmBZD8.cjs +2 -0
- package/dist/goals-DLbmBZD8.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +8 -8
- package/dist/measure/angleType.d.cts +2 -0
- package/dist/measure/angleType.d.ts +3 -0
- package/dist/measure/angleType.d.ts.map +1 -0
- package/dist/measure/geometryAdapter.d.cts +3 -0
- package/dist/measure/geometryAdapter.d.ts +3 -0
- package/dist/measure/geometryAdapter.d.ts.map +1 -1
- package/dist/react-B26_e52I.cjs +2 -0
- package/dist/react-B26_e52I.cjs.map +1 -0
- package/dist/{react-BEcdx1f6.js → react-B5cGaxH8.js} +738 -513
- package/dist/react-B5cGaxH8.js.map +1 -0
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/renderer/GeometryCanvas.d.ts.map +1 -1
- package/dist/renderer/StudentGeometryCanvas.d.ts.map +1 -1
- package/dist/renderer/Toolbar.d.ts.map +1 -1
- package/dist/renderer/angleMeasure.d.cts +14 -0
- package/dist/renderer/angleMeasure.d.ts +15 -0
- package/dist/renderer/angleMeasure.d.ts.map +1 -0
- package/dist/renderer/deriveRenderableGeometry.d.cts +1 -0
- package/dist/renderer/deriveRenderableGeometry.d.ts +1 -0
- package/dist/renderer/deriveRenderableGeometry.d.ts.map +1 -1
- package/dist/renderer/deriveSemanticScene.d.ts.map +1 -1
- package/dist/renderer/dragSnap.d.cts +14 -0
- package/dist/renderer/dragSnap.d.ts +15 -0
- package/dist/renderer/dragSnap.d.ts.map +1 -0
- package/dist/renderer/shapes/AngleShape.d.cts +19 -0
- package/dist/renderer/shapes/AngleShape.d.ts +20 -0
- package/dist/renderer/shapes/AngleShape.d.ts.map +1 -0
- package/dist/renderer/shapes/EntityLabel.d.cts +9 -1
- package/dist/renderer/shapes/EntityLabel.d.ts +9 -1
- package/dist/renderer/shapes/EntityLabel.d.ts.map +1 -1
- package/dist/renderer/shapes/EntityShape.d.cts +2 -1
- package/dist/renderer/shapes/EntityShape.d.ts +2 -1
- package/dist/renderer/shapes/EntityShape.d.ts.map +1 -1
- package/dist/renderer/useConnectionPreview.d.cts +12 -1
- package/dist/renderer/useConnectionPreview.d.ts +12 -1
- package/dist/renderer/useConnectionPreview.d.ts.map +1 -1
- package/dist/renderer/usePointerGestures.d.cts +1 -1
- package/dist/renderer/usePointerGestures.d.ts +1 -1
- package/dist/renderer/usePointerGestures.d.ts.map +1 -1
- package/dist/schemas/entityDefinition.schema.d.cts +7 -0
- package/dist/schemas/entityDefinition.schema.d.ts +7 -0
- package/dist/schemas/entityDefinition.schema.d.ts.map +1 -1
- package/dist/schemas/entityDocument.d.cts +34 -13
- package/dist/schemas/entityDocument.d.ts +34 -13
- package/dist/schemas/entityDocument.d.ts.map +1 -1
- package/dist/schemas/entityPresentation.schema.d.cts +3 -0
- package/dist/schemas/entityPresentation.schema.d.ts +3 -0
- package/dist/schemas/entityPresentation.schema.d.ts.map +1 -1
- package/dist/schemas/exercise.schema.d.cts +80 -2
- package/dist/schemas/exercise.schema.d.ts +80 -2
- package/dist/schemas/exercise.schema.d.ts.map +1 -1
- package/dist/schemas/exerciseV2.schema.d.cts +57 -0
- package/dist/schemas/exerciseV2.schema.d.ts +57 -0
- package/dist/schemas/exerciseV2.schema.d.ts.map +1 -1
- package/dist/schemas/exerciseV4.schema.d.cts +168 -0
- package/dist/schemas/exerciseV4.schema.d.ts +169 -0
- package/dist/schemas/exerciseV4.schema.d.ts.map +1 -0
- package/dist/schemas/geometryState.schema.d.cts +8 -0
- package/dist/schemas/geometryState.schema.d.ts +8 -0
- package/dist/schemas/geometryState.schema.d.ts.map +1 -1
- package/dist/schemas/goals/angleBisectorGoal.schema.d.cts +15 -0
- package/dist/schemas/goals/angleBisectorGoal.schema.d.ts +16 -0
- package/dist/schemas/goals/angleBisectorGoal.schema.d.ts.map +1 -0
- package/dist/schemas/goals/angleCriterion.schema.d.cts +28 -0
- package/dist/schemas/goals/angleCriterion.schema.d.ts +29 -0
- package/dist/schemas/goals/angleCriterion.schema.d.ts.map +1 -0
- package/dist/schemas/goals/angleMeasureGoal.schema.d.cts +26 -0
- package/dist/schemas/goals/angleMeasureGoal.schema.d.ts +27 -0
- package/dist/schemas/goals/angleMeasureGoal.schema.d.ts.map +1 -0
- package/dist/schemas/goals/createGoal.schema.d.cts +1 -0
- package/dist/schemas/goals/createGoal.schema.d.ts +1 -0
- package/dist/schemas/goals/createGoal.schema.d.ts.map +1 -1
- package/dist/schemas/goals/geometryGoal.schema.d.cts +61 -0
- package/dist/schemas/goals/geometryGoal.schema.d.ts +61 -0
- package/dist/schemas/goals/geometryGoal.schema.d.ts.map +1 -1
- package/dist/schemas/goals/givenAngleGoal.schema.d.cts +35 -0
- package/dist/schemas/goals/givenAngleGoal.schema.d.ts +36 -0
- package/dist/schemas/goals/givenAngleGoal.schema.d.ts.map +1 -0
- package/dist/schemas/goals/goalKindDeclaration.d.cts +7 -0
- package/dist/schemas/goals/goalKindDeclaration.d.ts +7 -0
- package/dist/schemas/goals/goalKindDeclaration.d.ts.map +1 -1
- package/dist/schemas/goals/parallelGoal.schema.d.cts +1 -0
- package/dist/schemas/goals/parallelGoal.schema.d.ts +1 -0
- package/dist/schemas/goals/parallelGoal.schema.d.ts.map +1 -1
- package/dist/schemas/goals/perpendicularGoal.schema.d.cts +1 -0
- package/dist/schemas/goals/perpendicularGoal.schema.d.ts +1 -0
- package/dist/schemas/goals/perpendicularGoal.schema.d.ts.map +1 -1
- package/dist/schemas/goals/throughPointsGoal.schema.d.cts +1 -0
- package/dist/schemas/goals/throughPointsGoal.schema.d.ts +1 -0
- package/dist/schemas/goals/throughPointsGoal.schema.d.ts.map +1 -1
- package/dist/schemas/objectTypeName.schema.d.cts +1 -0
- package/dist/schemas/objectTypeName.schema.d.ts +1 -0
- package/dist/schemas/objectTypeName.schema.d.ts.map +1 -1
- package/dist/schemas/v2ToV3.d.cts +15 -1
- package/dist/schemas/v2ToV3.d.ts +15 -1
- package/dist/schemas/v2ToV3.d.ts.map +1 -1
- package/dist/schemas/v3ToV4.d.cts +2 -1
- package/dist/schemas/v3ToV4.d.ts +2 -1
- package/dist/schemas/v3ToV4.d.ts.map +1 -1
- package/dist/schemas/v4ToV5.d.cts +7 -0
- package/dist/schemas/v4ToV5.d.ts +8 -0
- package/dist/schemas/v4ToV5.d.ts.map +1 -0
- package/dist/schemas.cjs +1 -1
- package/dist/schemas.mjs +3 -3
- package/dist/snapping/computeDragSnap.d.cts +30 -0
- package/dist/snapping/computeDragSnap.d.ts +31 -0
- package/dist/snapping/computeDragSnap.d.ts.map +1 -0
- package/dist/state/geometryContext.d.ts.map +1 -1
- package/dist/store/attemptChange.d.cts +1 -1
- package/dist/store/attemptChange.d.ts +1 -1
- package/dist/store/attemptChange.d.ts.map +1 -1
- package/dist/store/constructionCommand.d.cts +14 -2
- package/dist/store/constructionCommand.d.ts +14 -2
- package/dist/store/constructionCommand.d.ts.map +1 -1
- package/dist/store/constructionIdentity.d.cts +9 -0
- package/dist/store/constructionIdentity.d.ts +9 -0
- package/dist/store/constructionIdentity.d.ts.map +1 -1
- package/dist/store/studentStore.d.cts +7 -0
- package/dist/store/studentStore.d.ts +7 -0
- package/dist/store/studentStore.d.ts.map +1 -1
- package/dist/store/teacherStore.d.cts +16 -0
- package/dist/store/teacherStore.d.ts +16 -0
- package/dist/store/teacherStore.d.ts.map +1 -1
- package/dist/{studentStore-CEKTtMCh.js → studentStore-CyYa0N93.js} +502 -370
- package/dist/studentStore-CyYa0N93.js.map +1 -0
- package/dist/studentStore-ubldy4PJ.cjs +2 -0
- package/dist/studentStore-ubldy4PJ.cjs.map +1 -0
- package/dist/validation/getGeometryExerciseWarnings.d.cts +3 -2
- package/dist/validation/getGeometryExerciseWarnings.d.ts +3 -2
- package/dist/validation/getGeometryExerciseWarnings.d.ts.map +1 -1
- package/dist/validation/goals/angleBisector.d.cts +3 -0
- package/dist/validation/goals/angleBisector.d.ts +4 -0
- package/dist/validation/goals/angleBisector.d.ts.map +1 -0
- package/dist/validation/goals/angleCriterion.d.cts +12 -0
- package/dist/validation/goals/angleCriterion.d.ts +13 -0
- package/dist/validation/goals/angleCriterion.d.ts.map +1 -0
- package/dist/validation/goals/angleMeasure.d.cts +9 -0
- package/dist/validation/goals/angleMeasure.d.ts +10 -0
- package/dist/validation/goals/angleMeasure.d.ts.map +1 -0
- package/dist/validation/goals/create.d.cts +2 -2
- package/dist/validation/goals/create.d.ts +2 -2
- package/dist/validation/goals/create.d.ts.map +1 -1
- package/dist/validation/goals/givenAngle.d.cts +11 -0
- package/dist/validation/goals/givenAngle.d.ts +12 -0
- package/dist/validation/goals/givenAngle.d.ts.map +1 -0
- package/dist/validation/goals/goalKindHandler.d.cts +38 -1
- package/dist/validation/goals/goalKindHandler.d.ts +38 -1
- package/dist/validation/goals/goalKindHandler.d.ts.map +1 -1
- package/dist/validation/goals/index.d.cts +9 -2
- package/dist/validation/goals/index.d.ts +9 -2
- package/dist/validation/goals/index.d.ts.map +1 -1
- package/dist/validation/goals/midpoint.d.cts +2 -2
- package/dist/validation/goals/midpoint.d.ts +2 -2
- package/dist/validation/goals/midpoint.d.ts.map +1 -1
- package/dist/validation/goals/parallel.d.cts +2 -2
- package/dist/validation/goals/parallel.d.ts +2 -2
- package/dist/validation/goals/parallel.d.ts.map +1 -1
- package/dist/validation/goals/perpendicular.d.cts +2 -2
- package/dist/validation/goals/perpendicular.d.ts +2 -2
- package/dist/validation/goals/perpendicular.d.ts.map +1 -1
- package/dist/validation/goals/segmentLength.d.cts +2 -2
- package/dist/validation/goals/segmentLength.d.ts +2 -2
- package/dist/validation/goals/segmentLength.d.ts.map +1 -1
- package/dist/validation/goals/throughPoints.d.cts +2 -2
- package/dist/validation/goals/throughPoints.d.ts +2 -2
- package/dist/validation/goals/throughPoints.d.ts.map +1 -1
- package/dist/validation/types.d.cts +5 -1
- package/dist/validation/types.d.ts +5 -1
- package/dist/validation/types.d.ts.map +1 -1
- package/dist/validation/validateGeometryState.d.ts.map +1 -1
- package/dist/validation/validateGeometryStateInput.d.ts.map +1 -1
- package/dist/validation.cjs +1 -1
- package/dist/validation.cjs.map +1 -1
- package/dist/validation.mjs +106 -85
- package/dist/validation.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/exercise.schema-BV2b7rtN.cjs +0 -2
- package/dist/exercise.schema-BV2b7rtN.cjs.map +0 -1
- package/dist/exercise.schema-ckVBW9XA.js +0 -212
- package/dist/exercise.schema-ckVBW9XA.js.map +0 -1
- package/dist/geometryAttemptStatus-5L_q6-9l.js.map +0 -1
- package/dist/geometryAttemptStatus-N_XHIkiS.cjs +0 -2
- package/dist/geometryAttemptStatus-N_XHIkiS.cjs.map +0 -1
- package/dist/geometryState.schema-D0ixyGYE.js +0 -955
- package/dist/geometryState.schema-D0ixyGYE.js.map +0 -1
- package/dist/geometryState.schema-DKQ-durx.cjs +0 -2
- package/dist/geometryState.schema-DKQ-durx.cjs.map +0 -1
- package/dist/goals-C3ZwbEh_.js +0 -451
- package/dist/goals-C3ZwbEh_.js.map +0 -1
- package/dist/goals-CxR5qPDC.cjs +0 -2
- package/dist/goals-CxR5qPDC.cjs.map +0 -1
- package/dist/react-BEcdx1f6.js.map +0 -1
- package/dist/react-Pp_VSk--.cjs +0 -2
- package/dist/react-Pp_VSk--.cjs.map +0 -1
- package/dist/studentStore-CEKTtMCh.js.map +0 -1
- package/dist/studentStore-DLftfFAl.cjs +0 -2
- package/dist/studentStore-DLftfFAl.cjs.map +0 -1
package/dist/validation.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
1
|
+
import { J as e, M as t } from "./geometryState.schema-CKFPlX8j.js";
|
|
2
|
+
import { a as n, f as r, i, n as a, p as o, r as s, s as c, t as l, u } from "./goals-BlSeniyt.js";
|
|
3
|
+
import { a as d, c as f, d as p, i as m, l as h, n as g, o as _, r as v, s as y, t as b, u as x } from "./exerciseLoad-DgeDXYbf.js";
|
|
4
4
|
//#region src/validation/validateGeometryState.ts
|
|
5
|
-
function x(e, t) {
|
|
6
|
-
return S(e, t).result;
|
|
7
|
-
}
|
|
8
5
|
function S(e, t) {
|
|
9
|
-
return
|
|
6
|
+
return C(e, t).result;
|
|
10
7
|
}
|
|
11
8
|
function C(e, t) {
|
|
12
|
-
return
|
|
9
|
+
return T(e, t, !1).feedback;
|
|
10
|
+
}
|
|
11
|
+
function w(e, t) {
|
|
12
|
+
return T(e, t, !0);
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
let
|
|
16
|
-
if (
|
|
14
|
+
function T(e, a, c) {
|
|
15
|
+
let u = e.goals.length === 0, d = e.grading?.extraObjects === "reject", f = c || d;
|
|
16
|
+
if (u && !c) return {
|
|
17
17
|
feedback: {
|
|
18
18
|
result: {
|
|
19
19
|
status: "incorrect",
|
|
@@ -25,75 +25,96 @@ function w(t, a, s) {
|
|
|
25
25
|
diagnostics: [],
|
|
26
26
|
extraEntityIds: []
|
|
27
27
|
};
|
|
28
|
-
let
|
|
29
|
-
exercise:
|
|
30
|
-
points:
|
|
31
|
-
},
|
|
28
|
+
let p = r(e, a), m = {
|
|
29
|
+
exercise: e,
|
|
30
|
+
points: p.points
|
|
31
|
+
}, h = [], g = [], _ = [];
|
|
32
32
|
for (let n of a.answerEntityIds) {
|
|
33
|
-
if (e
|
|
34
|
-
let r =
|
|
33
|
+
if (t(e.entities, n)) continue;
|
|
34
|
+
let r = p.definitionOf(n);
|
|
35
35
|
if (r) {
|
|
36
|
-
if (
|
|
37
|
-
|
|
36
|
+
if (f && _.push(n), p.resolvedOf(n) === void 0) {
|
|
37
|
+
f && g.push({
|
|
38
38
|
id: n,
|
|
39
39
|
definition: r
|
|
40
40
|
});
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
h.push({
|
|
44
44
|
id: n,
|
|
45
45
|
definition: r
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
let
|
|
50
|
-
for (let
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
let v = [], y = [], b = /* @__PURE__ */ new Set();
|
|
50
|
+
for (let t of e.goals) {
|
|
51
|
+
let e = l(t);
|
|
52
|
+
if (e) {
|
|
53
|
+
let n = e.judgedEntityIds(t), r = e.satisfiedByFigure(t, {
|
|
54
|
+
...m,
|
|
55
|
+
scene: p
|
|
56
|
+
}), i = r === void 0 ? [] : [...n];
|
|
57
|
+
v.push({
|
|
58
|
+
id: t.id,
|
|
59
|
+
status: r === !0 ? "correct" : "incorrect",
|
|
60
|
+
constructionIds: i,
|
|
61
|
+
pointIds: []
|
|
62
|
+
}), c && y.push({
|
|
63
|
+
id: t.id,
|
|
64
|
+
status: r === !0 ? "correct" : "incorrect",
|
|
65
|
+
code: r === void 0 ? "unresolved-reference" : r ? "goal-satisfied" : "goal-not-satisfied",
|
|
66
|
+
matchedEntityIds: r === !0 ? [...n] : [],
|
|
67
|
+
relevantEntityIds: i,
|
|
68
|
+
unresolvedEntityIds: []
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let r = h.filter((e) => i(e, t, m));
|
|
73
|
+
if (f) for (let e of r) b.add(e.id);
|
|
74
|
+
let a = r.length > 0, u = a && !c ? [] : h.filter((e) => n(e, t, m)), d = a ? r : u;
|
|
75
|
+
if (v.push({
|
|
76
|
+
id: t.id,
|
|
77
|
+
status: a ? "correct" : "incorrect",
|
|
78
|
+
constructionIds: d.filter((e) => !o(e)).map((e) => e.id),
|
|
79
|
+
pointIds: d.filter(o).map((e) => e.id)
|
|
80
|
+
}), !c) continue;
|
|
81
|
+
let _ = g.filter((e) => s(e, t));
|
|
82
|
+
y.push({
|
|
83
|
+
id: t.id,
|
|
84
|
+
status: a ? "correct" : "incorrect",
|
|
85
|
+
code: a ? "goal-satisfied" : h.some((e) => s(e, t)) ? "goal-not-satisfied" : _.length > 0 ? "unresolved-candidate" : "no-candidate",
|
|
86
|
+
matchedEntityIds: D(r),
|
|
87
|
+
relevantEntityIds: D(u),
|
|
88
|
+
unresolvedEntityIds: D(_)
|
|
68
89
|
});
|
|
69
90
|
}
|
|
70
|
-
let
|
|
71
|
-
status: !
|
|
72
|
-
goals:
|
|
91
|
+
let x = f ? E(_, b, p) : [], S = {
|
|
92
|
+
status: !u && v.every((e) => e.status === "correct") && !(d && x.length > 0) ? "correct" : "incorrect",
|
|
93
|
+
goals: v.map(({ id: e, status: t }) => ({
|
|
73
94
|
id: e,
|
|
74
95
|
status: t
|
|
75
96
|
}))
|
|
76
|
-
},
|
|
77
|
-
return
|
|
97
|
+
}, C = [];
|
|
98
|
+
return u && C.push({
|
|
78
99
|
code: "no-goals",
|
|
79
100
|
entityIds: []
|
|
80
|
-
}),
|
|
101
|
+
}), g.length > 0 && C.push({
|
|
81
102
|
code: "unresolved-entity",
|
|
82
|
-
entityIds:
|
|
83
|
-
}),
|
|
103
|
+
entityIds: D(g)
|
|
104
|
+
}), d && x.length > 0 && C.push({
|
|
84
105
|
code: "extra-objects",
|
|
85
|
-
entityIds:
|
|
106
|
+
entityIds: x
|
|
86
107
|
}), {
|
|
87
108
|
feedback: {
|
|
88
|
-
result:
|
|
89
|
-
goals:
|
|
109
|
+
result: S,
|
|
110
|
+
goals: v
|
|
90
111
|
},
|
|
91
|
-
goals:
|
|
92
|
-
diagnostics:
|
|
93
|
-
extraEntityIds:
|
|
112
|
+
goals: y,
|
|
113
|
+
diagnostics: C,
|
|
114
|
+
extraEntityIds: x
|
|
94
115
|
};
|
|
95
116
|
}
|
|
96
|
-
function
|
|
117
|
+
function E(e, t, n) {
|
|
97
118
|
let r = new Set(t), i = [...t];
|
|
98
119
|
for (; i.length > 0;) {
|
|
99
120
|
let e = i.pop();
|
|
@@ -101,13 +122,13 @@ function T(e, t, n) {
|
|
|
101
122
|
}
|
|
102
123
|
return [...new Set(e)].filter((e) => !r.has(e));
|
|
103
124
|
}
|
|
104
|
-
function
|
|
125
|
+
function D(e) {
|
|
105
126
|
return [...new Set(e.map((e) => e.id))];
|
|
106
127
|
}
|
|
107
128
|
//#endregion
|
|
108
129
|
//#region src/validation/checkGeometryAnswer.ts
|
|
109
|
-
function
|
|
110
|
-
let n =
|
|
130
|
+
function O(e, t) {
|
|
131
|
+
let n = b(e);
|
|
111
132
|
if (!n.success) return {
|
|
112
133
|
success: !1,
|
|
113
134
|
data: null,
|
|
@@ -117,13 +138,13 @@ function D(e, t) {
|
|
|
117
138
|
})),
|
|
118
139
|
warnings: []
|
|
119
140
|
};
|
|
120
|
-
let r =
|
|
141
|
+
let r = x(t, n.data);
|
|
121
142
|
return r.success ? {
|
|
122
143
|
success: !0,
|
|
123
|
-
data:
|
|
144
|
+
data: C(n.data, r.data),
|
|
124
145
|
fingerprints: {
|
|
125
|
-
exercise:
|
|
126
|
-
attempt:
|
|
146
|
+
exercise: y(n.data),
|
|
147
|
+
attempt: _(r.data)
|
|
127
148
|
},
|
|
128
149
|
errors: [],
|
|
129
150
|
warnings: n.warnings
|
|
@@ -137,8 +158,8 @@ function D(e, t) {
|
|
|
137
158
|
warnings: []
|
|
138
159
|
};
|
|
139
160
|
}
|
|
140
|
-
function
|
|
141
|
-
let n =
|
|
161
|
+
function k(e, t) {
|
|
162
|
+
let n = b(e);
|
|
142
163
|
if (!n.success) return {
|
|
143
164
|
success: !1,
|
|
144
165
|
data: null,
|
|
@@ -149,13 +170,13 @@ function O(e, t) {
|
|
|
149
170
|
})),
|
|
150
171
|
warnings: []
|
|
151
172
|
};
|
|
152
|
-
let r =
|
|
173
|
+
let r = x(t, n.data);
|
|
153
174
|
return r.success ? {
|
|
154
175
|
success: !0,
|
|
155
|
-
data:
|
|
176
|
+
data: w(n.data, r.data),
|
|
156
177
|
fingerprints: {
|
|
157
|
-
exercise:
|
|
158
|
-
attempt:
|
|
178
|
+
exercise: y(n.data),
|
|
179
|
+
attempt: _(r.data)
|
|
159
180
|
},
|
|
160
181
|
errors: [],
|
|
161
182
|
warnings: n.warnings
|
|
@@ -172,11 +193,11 @@ function O(e, t) {
|
|
|
172
193
|
}
|
|
173
194
|
//#endregion
|
|
174
195
|
//#region src/validation/revealGeometryAnswers.ts
|
|
175
|
-
function
|
|
176
|
-
let t =
|
|
196
|
+
function A(e) {
|
|
197
|
+
let t = u(e);
|
|
177
198
|
return e.goals.map((n) => ({
|
|
178
199
|
id: n.id,
|
|
179
|
-
shape:
|
|
200
|
+
shape: c(n, {
|
|
180
201
|
exercise: e,
|
|
181
202
|
points: t
|
|
182
203
|
})
|
|
@@ -184,42 +205,42 @@ function k(e) {
|
|
|
184
205
|
}
|
|
185
206
|
//#endregion
|
|
186
207
|
//#region src/validation/revealGeometryAttempt.ts
|
|
187
|
-
function
|
|
208
|
+
function j(e) {
|
|
188
209
|
return {
|
|
189
210
|
success: !1,
|
|
190
211
|
data: null,
|
|
191
212
|
errors: e
|
|
192
213
|
};
|
|
193
214
|
}
|
|
194
|
-
function
|
|
195
|
-
let t =
|
|
196
|
-
if (!t.success) return
|
|
215
|
+
function M(e) {
|
|
216
|
+
let t = b(e);
|
|
217
|
+
if (!t.success) return j(t.errors.map((e) => ({
|
|
197
218
|
code: "invalid-exercise",
|
|
198
219
|
...e
|
|
199
220
|
})));
|
|
200
|
-
if (t.solution === void 0) return
|
|
221
|
+
if (t.solution === void 0) return j([{
|
|
201
222
|
code: "invalid-canonical-attempt",
|
|
202
223
|
path: ["solution"],
|
|
203
224
|
message: "this exercise has no canonical solution"
|
|
204
225
|
}]);
|
|
205
|
-
let n =
|
|
206
|
-
return n.success ?
|
|
226
|
+
let n = g(t.solution, t.data);
|
|
227
|
+
return n.success ? S(t.data, n.data).status === "correct" ? {
|
|
207
228
|
success: !0,
|
|
208
229
|
data: n.data,
|
|
209
230
|
errors: []
|
|
210
|
-
} :
|
|
231
|
+
} : j([{
|
|
211
232
|
code: "canonical-attempt-incorrect",
|
|
212
233
|
path: ["solution"],
|
|
213
234
|
message: "the canonical solution does not satisfy this exercise"
|
|
214
|
-
}]) :
|
|
235
|
+
}]) : j(n.errors.map((e) => ({
|
|
215
236
|
code: "invalid-canonical-attempt",
|
|
216
237
|
...e
|
|
217
238
|
})));
|
|
218
239
|
}
|
|
219
240
|
//#endregion
|
|
220
241
|
//#region src/validation/validateGeometryExercise.ts
|
|
221
|
-
function
|
|
222
|
-
let t =
|
|
242
|
+
function N(e) {
|
|
243
|
+
let t = b(e);
|
|
223
244
|
if (!t.success) return {
|
|
224
245
|
success: !1,
|
|
225
246
|
data: null,
|
|
@@ -232,7 +253,7 @@ function M(e) {
|
|
|
232
253
|
errors: [],
|
|
233
254
|
warnings: t.warnings
|
|
234
255
|
};
|
|
235
|
-
let n =
|
|
256
|
+
let n = g(t.solution, t.data);
|
|
236
257
|
return n.success ? {
|
|
237
258
|
success: !0,
|
|
238
259
|
data: {
|
|
@@ -248,9 +269,9 @@ function M(e) {
|
|
|
248
269
|
warnings: []
|
|
249
270
|
};
|
|
250
271
|
}
|
|
251
|
-
function
|
|
272
|
+
function P(e) {
|
|
252
273
|
try {
|
|
253
|
-
return
|
|
274
|
+
return N(JSON.parse(e));
|
|
254
275
|
} catch (e) {
|
|
255
276
|
return {
|
|
256
277
|
success: !1,
|
|
@@ -264,6 +285,6 @@ function N(e) {
|
|
|
264
285
|
}
|
|
265
286
|
}
|
|
266
287
|
//#endregion
|
|
267
|
-
export {
|
|
288
|
+
export { O as checkGeometryAnswer, k as checkGeometryAnswerDetailed, m as createEmptyGeometryAttempt, f as deriveGeometryCanvasFeedback, C as evaluateGeometryState, w as evaluateGeometryStateDetailed, v as getGeometryExerciseWarnings, a as getGoalKindHandler, d as hasGeometryWork, e as intersectionsOf, A as revealGeometryAnswers, M as revealGeometryAttempt, h as serializeGeometryState, N as validateGeometryExercise, P as validateGeometryExerciseJson, S as validateGeometryState, x as validateGeometryStateInput, p as validateGeometryStateJson };
|
|
268
289
|
|
|
269
290
|
//# sourceMappingURL=validation.mjs.map
|
package/dist/validation.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.mjs","names":[],"sources":["../src/validation/validateGeometryState.ts","../src/validation/checkGeometryAnswer.ts","../src/validation/revealGeometryAnswers.ts","../src/validation/revealGeometryAttempt.ts","../src/validation/validateGeometryExercise.ts"],"sourcesContent":["import { entryOf } from '../geometry/entityRecord'\nimport type { EntityId } from '../geometry/entityModel'\nimport type { GeometryExercise } from '../schemas/exercise.schema'\nimport type { GeometryState } from '../schemas/geometryState.schema'\nimport { isCandidatePoint, type GoalCandidate } from '../geometry/goalCandidate'\nimport { buildGeometryContext } from '../state/geometryContext'\nimport type {\n DetailedGeometryFeedback,\n DetailedGoalResult,\n GeometryDiagnostic,\n GeometryValidationFeedback,\n GoalGeometryFeedback,\n ValidationContext,\n ValidationResult,\n} from './types'\nimport { goalAcceptsCandidate, goalSatisfiedBy, isRelevantGoalAttempt } from './goals'\n\nexport function validateGeometryState(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): ValidationResult {\n return evaluateGeometryState(exercise, geometry).result\n}\n\nexport function evaluateGeometryState(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): GeometryValidationFeedback {\n return evaluate(exercise, geometry, false).feedback\n}\n\n/**\n * The same grading, reported rather than summarized: why each goal landed where\n * it did, as stable codes and entity ids (RFC 0004 § Decision 5). It measures\n * nothing the plain evaluation does not — the codes are read off the existing\n * goal handlers' own predicates — so the two can never disagree.\n *\n * `feedback` is exactly what `evaluateGeometryState()` returns for the same\n * inputs. The extra work is one relevance pass per satisfied goal and the\n * unresolved bookkeeping, which the plain path still skips.\n */\nexport function evaluateGeometryStateDetailed(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): DetailedGeometryFeedback {\n return evaluate(exercise, geometry, true)\n}\n\nfunction evaluate(\n exercise: GeometryExercise,\n geometry: GeometryState,\n detailed: boolean,\n): DetailedGeometryFeedback {\n // An exercise nobody can answer is incorrect, and the report now says so in\n // one word rather than leaving a host to infer it from an empty goal list.\n // The plain path stops here, as it always has; the report does not, because\n // an unresolved answer entity is reported whatever the goals say — there is\n // simply no goal loop to run below.\n const noGoals = exercise.goals.length === 0\n // Absence means `allow` (ADR 0013), so an exercise authored before v3 keeps\n // exactly the verdict it had. The extra-object sets cost a closure walk, so\n // the plain path computes them only when the policy actually reads them:\n // under `allow` they are reported and never graded on.\n const strict = exercise.grading?.extraObjects === 'reject'\n const needsExtras = detailed || strict\n if (noGoals && !detailed) {\n return {\n feedback: { result: { status: 'incorrect', goals: [] }, goals: [] },\n goals: [],\n diagnostics: [],\n extraEntityIds: [],\n }\n }\n\n // Coordinates come from the dependency graph, not from storage: a derived\n // point is wherever its parents currently put it. A point that cannot be\n // resolved (its object was deleted, its intersection vanished) is absent\n // here and therefore cannot satisfy anything.\n const scene = buildGeometryContext(exercise, geometry)\n // The scene's own index, not a copy: `ValidationContext.points` is read-only\n // by contract (M2.3.c), so there is nothing to defend it against. The copy\n // this replaced was one `Map` allocation and one full rehash per validation.\n const context: ValidationContext = { exercise, points: scene.points }\n // One list for every kind, which is what `answerEntityIds` is: v1 kept a\n // candidacy list for points only and treated every construction as a\n // candidate, and the migration reproduces that by naming both. A goal kind\n // decides for itself which candidates it can be satisfied by (a midpoint goal\n // wants a point, a perpendicular goal wants a segment or line); points the\n // student only used as scaffolding are not named here and are never\n // candidates.\n //\n // An entity that no longer resolves has no coordinates to measure, so it is\n // dropped here rather than left for a handler to trip over: handlers may\n // assume every candidate they are handed can be looked up in `points`. An\n // entity resolves exactly when everything it is built on does, which is the\n // index's answer rather than a second scan.\n const candidates: GoalCandidate[] = []\n // Answer entities that exist but have no coordinates this evaluation. The\n // plain path drops them silently; the report names them, because \"you built\n // it and then deleted what it stood on\" is not the same as \"you built\n // nothing\".\n const unresolved: GoalCandidate[] = []\n // Set `A` of RFC 0004 § Decision 6, in answer order: every id the attempt\n // owns and names as an answer, resolved or not. The extra-object policy\n // subtracts from this list and nothing else, so scaffolding the student never\n // offered as an answer cannot be an extra.\n const answered: EntityId[] = []\n for (const id of geometry.answerEntityIds) {\n // A given is not an answer to anything (ADR 0008 § Amendment), whatever\n // the list says. `validateGeometryStateInput()` already strips these on\n // load, but grading takes both documents directly — a host holding a\n // migrated v1 attempt it never loaded through the engine, or a store still\n // in memory, would otherwise have the exercise's own given graded as the\n // student's construction (M2.G.2).\n if (entryOf(exercise.entities, id)) continue\n const definition = scene.definitionOf(id)\n if (!definition) continue\n if (needsExtras) answered.push(id)\n if (scene.resolvedOf(id) === undefined) {\n if (needsExtras) unresolved.push({ id, definition })\n continue\n }\n candidates.push({ id, definition })\n }\n const goals: GoalGeometryFeedback[] = []\n const detailedGoals: DetailedGoalResult[] = []\n // Set `M`: every candidate that satisfies *some* goal. A union, not an\n // assignment — one entity may answer two goals and two entities may answer\n // one, and this policy deliberately does not count solutions.\n const matched = new Set<EntityId>()\n for (const goal of exercise.goals) {\n const matching = candidates.filter((candidate) => goalSatisfiedBy(candidate, goal, context))\n if (needsExtras) for (const candidate of matching) matched.add(candidate.id)\n const correct = matching.length > 0\n // Highlighting wants relevance only when the goal is unsatisfied; the\n // report wants it either way, so a correct goal still says which other\n // attempts were aimed at it.\n const relevant =\n correct && !detailed\n ? []\n : candidates.filter((candidate) => isRelevantGoalAttempt(candidate, goal, context))\n const shown = correct ? matching : relevant\n goals.push({\n id: goal.id,\n status: correct ? 'correct' : 'incorrect',\n constructionIds: shown.filter((candidate) => !isCandidatePoint(candidate)).map((c) => c.id),\n pointIds: shown.filter(isCandidatePoint).map((candidate) => candidate.id),\n })\n if (!detailed) continue\n const unresolvedForGoal = unresolved.filter((candidate) =>\n goalAcceptsCandidate(candidate, goal),\n )\n detailedGoals.push({\n id: goal.id,\n status: correct ? 'correct' : 'incorrect',\n code: correct\n ? 'goal-satisfied'\n : candidates.some((candidate) => goalAcceptsCandidate(candidate, goal))\n ? 'goal-not-satisfied'\n : unresolvedForGoal.length > 0\n ? 'unresolved-candidate'\n : 'no-candidate',\n matchedEntityIds: distinctIds(matching),\n relevantEntityIds: distinctIds(relevant),\n unresolvedEntityIds: distinctIds(unresolvedForGoal),\n })\n }\n // `A − (M ∪ S)`: answer entities no goal needed, and that nothing a goal\n // needed was built on. Empty unless the policy or the report asked for it.\n const extraEntityIds = needsExtras ? withoutSupport(answered, matched, scene) : []\n const goalsMet = !noGoals && goals.every((goal) => goal.status === 'correct')\n const result: ValidationResult = {\n // `every` on an empty list is vacuously true, and a goalless exercise is\n // incorrect — the one place the two disagree.\n //\n // Under `reject`, an extra answer entity makes the attempt incorrect while\n // every goal stays as its own predicate found it: \"the goals are met and\n // something unused is still on the canvas\" is a different thing from a\n // failed goal, and the diagnostic below is where it is said.\n status: goalsMet && !(strict && extraEntityIds.length > 0) ? 'correct' : 'incorrect',\n goals: goals.map(({ id, status }) => ({ id, status })),\n }\n const diagnostics: GeometryDiagnostic[] = []\n if (noGoals) diagnostics.push({ code: 'no-goals', entityIds: [] })\n // Informational: an unresolved entity does not make a satisfied exercise\n // incorrect. Reporting it regardless is what lets a host say \"this one is\n // no longer on the canvas\" instead of leaving it unexplained.\n if (unresolved.length > 0) {\n diagnostics.push({ code: 'unresolved-entity', entityIds: distinctIds(unresolved) })\n }\n // Only under `reject`: the list is reported either way, but it is a finding\n // about the attempt only where the author asked for strictness. Last, because\n // an unresolved extra is explained by the diagnostic above it.\n if (strict && extraEntityIds.length > 0) {\n diagnostics.push({ code: 'extra-objects', entityIds: extraEntityIds })\n }\n return { feedback: { result, goals }, goals: detailedGoals, diagnostics, extraEntityIds }\n}\n\n/**\n * `answered` minus the matched entities and minus set `S`, their transitive\n * parents — the support closure.\n *\n * Support is exempt because it is not an alternative answer: the two points a\n * correct segment is drawn between, and the helper objects a correct derived\n * point stands on, are the construction, however the student named them. That\n * is why the walk goes *up* the graph from what satisfied a goal rather than\n * down from every answer entity.\n *\n * An unresolved answer entity cannot be in `matched` — nothing measured it —\n * and it can only be support if something that did resolve was built on it,\n * which is impossible. So it stays, which is the reported outcome RFC 0004 §\n * Decision 6 names.\n */\nfunction withoutSupport(\n answered: readonly EntityId[],\n matched: ReadonlySet<EntityId>,\n scene: { parentsOf(id: EntityId): readonly EntityId[] },\n): EntityId[] {\n const exempt = new Set<EntityId>(matched)\n const pending = [...matched]\n // Visited-bounded, because the graph is walked by id: a cycle is\n // unrepresentable in a stored document, but this must not depend on that.\n while (pending.length > 0) {\n const id = pending.pop()\n if (id === undefined) continue\n for (const parent of scene.parentsOf(id)) {\n if (exempt.has(parent)) continue\n exempt.add(parent)\n pending.push(parent)\n }\n }\n return [...new Set(answered)].filter((id) => !exempt.has(id))\n}\n\n/** `answerEntityIds` order, each id once — the reporting order RFC 0004 fixes. */\nfunction distinctIds(candidates: readonly GoalCandidate[]): string[] {\n return [...new Set(candidates.map((candidate) => candidate.id))]\n}\n","import type { DetailedGeometryFeedback, GeometryValidationFeedback } from './types'\nimport type { GeometryExerciseWarning } from './getGeometryExerciseWarnings'\nimport { fingerprintGeometryAttempt, fingerprintGeometryExercise } from './gradingFingerprints'\nimport { loadExerciseDocument } from './exerciseLoad'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\nimport { evaluateGeometryState, evaluateGeometryStateDetailed } from './validateGeometryState'\nimport { validateGeometryStateInput } from './validateGeometryStateInput'\n\nexport interface CheckGeometryAnswerIssue extends GeometryExerciseValidationIssue {\n source: 'exercise' | 'geometry'\n}\n\n/**\n * Which grading inputs a successful check described (RFC 0005 § Decision 2).\n * Both are opaque and engine-produced: equal for equal normalized content,\n * whatever the object identity or key order, and the exercise member ignores\n * the authored solution. A host passes the whole result on and never computes\n * or compares these itself.\n */\nexport interface CheckGeometryAnswerFingerprints {\n exercise: string\n attempt: string\n}\n\nexport type CheckGeometryAnswerResult =\n | {\n success: true\n data: GeometryValidationFeedback\n fingerprints: CheckGeometryAnswerFingerprints\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: CheckGeometryAnswerIssue[]\n warnings: []\n }\n\n/**\n * Safe public Check entry point for untrusted application data. Structure is\n * validated first; valid geometry is then evaluated by the JSXGraph-backed\n * goal handlers used by validateGeometryState().\n */\nexport function checkGeometryAnswer(\n exerciseInput: unknown,\n geometryInput: unknown,\n): CheckGeometryAnswerResult {\n // The solution-free load: grading ignores the authored solution, so a\n // malformed one cannot fail a check (ADR 0015).\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return {\n success: false,\n data: null,\n errors: exercise.errors.map((issue) => ({ ...issue, source: 'exercise' })),\n warnings: [],\n }\n }\n\n const geometry = validateGeometryStateInput(geometryInput, exercise.data)\n if (!geometry.success) {\n return {\n success: false,\n data: null,\n errors: geometry.errors.map((issue) => ({ ...issue, source: 'geometry' })),\n warnings: [],\n }\n }\n\n return {\n success: true,\n data: evaluateGeometryState(exercise.data, geometry.data),\n fingerprints: {\n exercise: fingerprintGeometryExercise(exercise.data),\n attempt: fingerprintGeometryAttempt(geometry.data),\n },\n errors: [],\n warnings: exercise.warnings,\n }\n}\n\n/**\n * Which document was rejected, as a code rather than a sentence. `message`\n * stays the schema's human-readable detail and is never a reason code: a\n * malformed document is not a student's wrong answer, and nothing here reaches\n * geometry evaluation (RFC 0004 § Decision 5).\n */\nexport type CheckGeometryAnswerErrorCode = 'invalid-exercise' | 'invalid-attempt'\n\nexport interface DetailedCheckGeometryAnswerIssue extends CheckGeometryAnswerIssue {\n code: CheckGeometryAnswerErrorCode\n}\n\nexport type DetailedCheckGeometryAnswerResult =\n | {\n success: true\n data: DetailedGeometryFeedback\n fingerprints: CheckGeometryAnswerFingerprints\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: DetailedCheckGeometryAnswerIssue[]\n warnings: []\n }\n\n/**\n * `checkGeometryAnswer()` with the structured report: same untrusted inputs,\n * same success discriminant, fingerprints and warnings, detailed data on\n * success.\n */\nexport function checkGeometryAnswerDetailed(\n exerciseInput: unknown,\n geometryInput: unknown,\n): DetailedCheckGeometryAnswerResult {\n // The solution-free load: grading ignores the authored solution, so a\n // malformed one cannot fail a check (ADR 0015).\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return {\n success: false,\n data: null,\n errors: exercise.errors.map((issue) => ({\n ...issue,\n source: 'exercise',\n code: 'invalid-exercise',\n })),\n warnings: [],\n }\n }\n\n const geometry = validateGeometryStateInput(geometryInput, exercise.data)\n if (!geometry.success) {\n return {\n success: false,\n data: null,\n errors: geometry.errors.map((issue) => ({\n ...issue,\n source: 'geometry',\n code: 'invalid-attempt',\n })),\n warnings: [],\n }\n }\n\n return {\n success: true,\n data: evaluateGeometryStateDetailed(exercise.data, geometry.data),\n fingerprints: {\n exercise: fingerprintGeometryExercise(exercise.data),\n attempt: fingerprintGeometryAttempt(geometry.data),\n },\n errors: [],\n warnings: exercise.warnings,\n }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { resolveExercisePoints } from '../state/resolvePoints'\nimport { revealedAnswerFor } from './goals'\nimport type { RevealedAnswer } from './types'\n\nexport function revealGeometryAnswers(exercise: GeometryExercise): RevealedAnswer[] {\n // The exercise's own points, resolved: a canonical answer is measured\n // against the givens and nothing else, so there is no attempt to index here.\n const points = resolveExercisePoints(exercise)\n\n return exercise.goals.map((goal) => ({\n id: goal.id,\n shape: revealedAnswerFor(goal, { exercise, points }),\n }))\n}\n","import type { GeometryState } from '../schemas/geometryState.schema'\nimport { loadExerciseDocument, validateEmbeddedSolution } from './exerciseLoad'\nimport { validateGeometryState } from './validateGeometryState'\n\n/**\n * Why no canonical attempt came back, as a code rather than a sentence (RFC\n * 0005 § Decision 4). The three are kept apart because a host acts on them\n * differently: a broken exercise is the whole widget's problem, a missing or\n * malformed solution is an authoring gap, and an incorrect one means the\n * authored answer and the goals disagree.\n */\nexport type RevealGeometryAttemptErrorCode =\n 'invalid-exercise' | 'invalid-canonical-attempt' | 'canonical-attempt-incorrect'\n\nexport interface RevealGeometryAttemptIssue {\n code: RevealGeometryAttemptErrorCode\n path: Array<string | number>\n message: string\n}\n\nexport type RevealGeometryAttemptResult =\n | { success: true; data: GeometryState; errors: [] }\n | { success: false; data: null; errors: RevealGeometryAttemptIssue[] }\n\nfunction refused(errors: RevealGeometryAttemptIssue[]): RevealGeometryAttemptResult {\n return { success: false, data: null, errors }\n}\n\n/**\n * The authored canonical attempt an exercise embeds, for a host's Reveal\n * (ADR 0015). Synchronous and DOM-free, and safe on untrusted input: the\n * exercise is migrated and parsed, then its `solution` is migrated, parsed and\n * normalized as an attempt against that exercise, then graded under the\n * exercise's own policy. Only a solution that grades correct is returned.\n *\n * Nothing is synthesized from goals and nothing is merged with student work:\n * the host replaces its attempt value with `data`. Paths of a malformed\n * solution are rooted at `solution`, so they point into the exercise document\n * the host holds.\n */\nexport function revealGeometryAttempt(exerciseInput: unknown): RevealGeometryAttemptResult {\n // The same two stages `validateGeometryExercise()` runs, kept apart here only\n // so each failure gets its own code.\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return refused(exercise.errors.map((issue) => ({ code: 'invalid-exercise', ...issue })))\n }\n\n if (exercise.solution === undefined) {\n return refused([\n {\n code: 'invalid-canonical-attempt',\n path: ['solution'],\n message: 'this exercise has no canonical solution',\n },\n ])\n }\n\n const attempt = validateEmbeddedSolution(exercise.solution, exercise.data)\n if (!attempt.success) {\n return refused(attempt.errors.map((issue) => ({ code: 'invalid-canonical-attempt', ...issue })))\n }\n\n if (validateGeometryState(exercise.data, attempt.data).status !== 'correct') {\n return refused([\n {\n code: 'canonical-attempt-incorrect',\n path: ['solution'],\n message: 'the canonical solution does not satisfy this exercise',\n },\n ])\n }\n\n return { success: true, data: attempt.data, errors: [] }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { loadExerciseDocument, validateEmbeddedSolution } from './exerciseLoad'\nimport type { GeometryExerciseWarning } from './getGeometryExerciseWarnings'\n\nexport interface GeometryExerciseValidationIssue {\n path: Array<string | number>\n message: string\n}\n\nexport type GeometryExerciseValidationResult =\n | {\n success: true\n data: GeometryExercise\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: GeometryExerciseValidationIssue[]\n warnings: []\n }\n\n/**\n * The whole stored exercise, authored solution included. Stored exercises are\n * brought up to the current version before their structure is checked, exactly\n * as attempts are (M2.6.b), and an embedded `solution` is then migrated and\n * checked against the exercise as an attempt (ADR 0015), with its issues rooted\n * at `solution`. On success `data.solution` is that normalized attempt.\n *\n * Grading does not go through here: Check and the student component load the\n * solution-free projection, so a broken solution never disables them. An\n * authoring tool does, and learns about it at save time.\n */\nexport function validateGeometryExercise(input: unknown): GeometryExerciseValidationResult {\n const loaded = loadExerciseDocument(input)\n if (!loaded.success) {\n return { success: false, data: null, errors: loaded.errors, warnings: [] }\n }\n if (loaded.solution === undefined) {\n return { success: true, data: loaded.data, errors: [], warnings: loaded.warnings }\n }\n\n const solution = validateEmbeddedSolution(loaded.solution, loaded.data)\n if (!solution.success) {\n return { success: false, data: null, errors: solution.errors, warnings: [] }\n }\n return {\n success: true,\n data: { ...loaded.data, solution: solution.data },\n errors: [],\n warnings: loaded.warnings,\n }\n}\n\nexport function validateGeometryExerciseJson(json: string): GeometryExerciseValidationResult {\n try {\n return validateGeometryExercise(JSON.parse(json))\n } catch (error) {\n return {\n success: false,\n data: null,\n errors: [\n {\n path: [],\n message: error instanceof Error ? error.message : 'Invalid JSON syntax',\n },\n ],\n warnings: [],\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.mjs","names":[],"sources":["../src/validation/validateGeometryState.ts","../src/validation/checkGeometryAnswer.ts","../src/validation/revealGeometryAnswers.ts","../src/validation/revealGeometryAttempt.ts","../src/validation/validateGeometryExercise.ts"],"sourcesContent":["import { entryOf } from '../geometry/entityRecord'\nimport type { EntityId } from '../geometry/entityModel'\nimport type { GeometryExercise } from '../schemas/exercise.schema'\nimport type { GeometryState } from '../schemas/geometryState.schema'\nimport { isCandidatePoint, type GoalCandidate } from '../geometry/goalCandidate'\nimport { buildGeometryContext } from '../state/geometryContext'\nimport type {\n DetailedGeometryFeedback,\n DetailedGoalResult,\n GeometryDiagnostic,\n GeometryValidationFeedback,\n GoalGeometryFeedback,\n ValidationContext,\n ValidationResult,\n} from './types'\nimport {\n figureGoalHandlerFor,\n goalAcceptsCandidate,\n goalSatisfiedBy,\n isRelevantGoalAttempt,\n} from './goals'\n\nexport function validateGeometryState(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): ValidationResult {\n return evaluateGeometryState(exercise, geometry).result\n}\n\nexport function evaluateGeometryState(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): GeometryValidationFeedback {\n return evaluate(exercise, geometry, false).feedback\n}\n\n/**\n * The same grading, reported rather than summarized: why each goal landed where\n * it did, as stable codes and entity ids (RFC 0004 § Decision 5). It measures\n * nothing the plain evaluation does not — the codes are read off the existing\n * goal handlers' own predicates — so the two can never disagree.\n *\n * `feedback` is exactly what `evaluateGeometryState()` returns for the same\n * inputs. The extra work is one relevance pass per satisfied goal and the\n * unresolved bookkeeping, which the plain path still skips.\n */\nexport function evaluateGeometryStateDetailed(\n exercise: GeometryExercise,\n geometry: GeometryState,\n): DetailedGeometryFeedback {\n return evaluate(exercise, geometry, true)\n}\n\nfunction evaluate(\n exercise: GeometryExercise,\n geometry: GeometryState,\n detailed: boolean,\n): DetailedGeometryFeedback {\n // An exercise nobody can answer is incorrect, and the report now says so in\n // one word rather than leaving a host to infer it from an empty goal list.\n // The plain path stops here, as it always has; the report does not, because\n // an unresolved answer entity is reported whatever the goals say — there is\n // simply no goal loop to run below.\n const noGoals = exercise.goals.length === 0\n // Absence means `allow` (ADR 0013), so an exercise authored before v3 keeps\n // exactly the verdict it had. The extra-object sets cost a closure walk, so\n // the plain path computes them only when the policy actually reads them:\n // under `allow` they are reported and never graded on.\n const strict = exercise.grading?.extraObjects === 'reject'\n const needsExtras = detailed || strict\n if (noGoals && !detailed) {\n return {\n feedback: { result: { status: 'incorrect', goals: [] }, goals: [] },\n goals: [],\n diagnostics: [],\n extraEntityIds: [],\n }\n }\n\n // Coordinates come from the dependency graph, not from storage: a derived\n // point is wherever its parents currently put it. A point that cannot be\n // resolved (its object was deleted, its intersection vanished) is absent\n // here and therefore cannot satisfy anything.\n const scene = buildGeometryContext(exercise, geometry)\n // The scene's own index, not a copy: `ValidationContext.points` is read-only\n // by contract (M2.3.c), so there is nothing to defend it against. The copy\n // this replaced was one `Map` allocation and one full rehash per validation.\n const context: ValidationContext = { exercise, points: scene.points }\n // One list for every kind, which is what `answerEntityIds` is: v1 kept a\n // candidacy list for points only and treated every construction as a\n // candidate, and the migration reproduces that by naming both. A goal kind\n // decides for itself which candidates it can be satisfied by (a midpoint goal\n // wants a point, a perpendicular goal wants a segment or line); points the\n // student only used as scaffolding are not named here and are never\n // candidates.\n //\n // An entity that no longer resolves has no coordinates to measure, so it is\n // dropped here rather than left for a handler to trip over: handlers may\n // assume every candidate they are handed can be looked up in `points`. An\n // entity resolves exactly when everything it is built on does, which is the\n // index's answer rather than a second scan.\n const candidates: GoalCandidate[] = []\n // Answer entities that exist but have no coordinates this evaluation. The\n // plain path drops them silently; the report names them, because \"you built\n // it and then deleted what it stood on\" is not the same as \"you built\n // nothing\".\n const unresolved: GoalCandidate[] = []\n // Set `A` of RFC 0004 § Decision 6, in answer order: every id the attempt\n // owns and names as an answer, resolved or not. The extra-object policy\n // subtracts from this list and nothing else, so scaffolding the student never\n // offered as an answer cannot be an extra.\n const answered: EntityId[] = []\n for (const id of geometry.answerEntityIds) {\n // A given is not an answer to anything (ADR 0008 § Amendment), whatever\n // the list says. `validateGeometryStateInput()` already strips these on\n // load, but grading takes both documents directly — a host holding a\n // migrated v1 attempt it never loaded through the engine, or a store still\n // in memory, would otherwise have the exercise's own given graded as the\n // student's construction (M2.G.2).\n if (entryOf(exercise.entities, id)) continue\n const definition = scene.definitionOf(id)\n if (!definition) continue\n if (needsExtras) answered.push(id)\n if (scene.resolvedOf(id) === undefined) {\n if (needsExtras) unresolved.push({ id, definition })\n continue\n }\n candidates.push({ id, definition })\n }\n const goals: GoalGeometryFeedback[] = []\n const detailedGoals: DetailedGoalResult[] = []\n // Set `M`: every candidate that satisfies *some* goal. A union, not an\n // assignment — one entity may answer two goals and two entities may answer\n // one, and this policy deliberately does not count solutions.\n const matched = new Set<EntityId>()\n for (const goal of exercise.goals) {\n // A figure goal judges a given entity as the figure resolves it, so it runs\n // outside the candidate filter entirely and adds nothing to `matched`: it\n // has no candidates, and ADR 0013's extra-object set is about answers\n // (ADR 0016, RFC 0006 § Reporting).\n const figure = figureGoalHandlerFor(goal)\n if (figure) {\n const judged = figure.judgedEntityIds(goal)\n const satisfied = figure.satisfiedByFigure(goal, { ...context, scene })\n // Unresolved is neither correct nor an answer that measured wrong, so\n // the judged id is reported on neither row.\n const shown = satisfied === undefined ? [] : [...judged]\n goals.push({\n id: goal.id,\n status: satisfied === true ? 'correct' : 'incorrect',\n // Every figure goal judges an angle, which is a construction; a figure\n // goal over a point would report it here too, and no kind has one.\n constructionIds: shown,\n pointIds: [],\n })\n if (detailed) {\n detailedGoals.push({\n id: goal.id,\n status: satisfied === true ? 'correct' : 'incorrect',\n code:\n satisfied === undefined\n ? 'unresolved-reference'\n : satisfied\n ? 'goal-satisfied'\n : 'goal-not-satisfied',\n matchedEntityIds: satisfied === true ? [...judged] : [],\n relevantEntityIds: shown,\n // `no-candidate` is never produced for a figure goal, and neither is\n // an unresolved *candidate*: what did not resolve is the exercise's\n // own entity, which the code above names.\n unresolvedEntityIds: [],\n })\n }\n continue\n }\n const matching = candidates.filter((candidate) => goalSatisfiedBy(candidate, goal, context))\n if (needsExtras) for (const candidate of matching) matched.add(candidate.id)\n const correct = matching.length > 0\n // Highlighting wants relevance only when the goal is unsatisfied; the\n // report wants it either way, so a correct goal still says which other\n // attempts were aimed at it.\n const relevant =\n correct && !detailed\n ? []\n : candidates.filter((candidate) => isRelevantGoalAttempt(candidate, goal, context))\n const shown = correct ? matching : relevant\n goals.push({\n id: goal.id,\n status: correct ? 'correct' : 'incorrect',\n constructionIds: shown.filter((candidate) => !isCandidatePoint(candidate)).map((c) => c.id),\n pointIds: shown.filter(isCandidatePoint).map((candidate) => candidate.id),\n })\n if (!detailed) continue\n const unresolvedForGoal = unresolved.filter((candidate) =>\n goalAcceptsCandidate(candidate, goal),\n )\n detailedGoals.push({\n id: goal.id,\n status: correct ? 'correct' : 'incorrect',\n code: correct\n ? 'goal-satisfied'\n : candidates.some((candidate) => goalAcceptsCandidate(candidate, goal))\n ? 'goal-not-satisfied'\n : unresolvedForGoal.length > 0\n ? 'unresolved-candidate'\n : 'no-candidate',\n matchedEntityIds: distinctIds(matching),\n relevantEntityIds: distinctIds(relevant),\n unresolvedEntityIds: distinctIds(unresolvedForGoal),\n })\n }\n // `A − (M ∪ S)`: answer entities no goal needed, and that nothing a goal\n // needed was built on. Empty unless the policy or the report asked for it.\n const extraEntityIds = needsExtras ? withoutSupport(answered, matched, scene) : []\n const goalsMet = !noGoals && goals.every((goal) => goal.status === 'correct')\n const result: ValidationResult = {\n // `every` on an empty list is vacuously true, and a goalless exercise is\n // incorrect — the one place the two disagree.\n //\n // Under `reject`, an extra answer entity makes the attempt incorrect while\n // every goal stays as its own predicate found it: \"the goals are met and\n // something unused is still on the canvas\" is a different thing from a\n // failed goal, and the diagnostic below is where it is said.\n status: goalsMet && !(strict && extraEntityIds.length > 0) ? 'correct' : 'incorrect',\n goals: goals.map(({ id, status }) => ({ id, status })),\n }\n const diagnostics: GeometryDiagnostic[] = []\n if (noGoals) diagnostics.push({ code: 'no-goals', entityIds: [] })\n // Informational: an unresolved entity does not make a satisfied exercise\n // incorrect. Reporting it regardless is what lets a host say \"this one is\n // no longer on the canvas\" instead of leaving it unexplained.\n if (unresolved.length > 0) {\n diagnostics.push({ code: 'unresolved-entity', entityIds: distinctIds(unresolved) })\n }\n // Only under `reject`: the list is reported either way, but it is a finding\n // about the attempt only where the author asked for strictness. Last, because\n // an unresolved extra is explained by the diagnostic above it.\n if (strict && extraEntityIds.length > 0) {\n diagnostics.push({ code: 'extra-objects', entityIds: extraEntityIds })\n }\n return { feedback: { result, goals }, goals: detailedGoals, diagnostics, extraEntityIds }\n}\n\n/**\n * `answered` minus the matched entities and minus set `S`, their transitive\n * parents — the support closure.\n *\n * Support is exempt because it is not an alternative answer: the two points a\n * correct segment is drawn between, and the helper objects a correct derived\n * point stands on, are the construction, however the student named them. That\n * is why the walk goes *up* the graph from what satisfied a goal rather than\n * down from every answer entity.\n *\n * An unresolved answer entity cannot be in `matched` — nothing measured it —\n * and it can only be support if something that did resolve was built on it,\n * which is impossible. So it stays, which is the reported outcome RFC 0004 §\n * Decision 6 names.\n */\nfunction withoutSupport(\n answered: readonly EntityId[],\n matched: ReadonlySet<EntityId>,\n scene: { parentsOf(id: EntityId): readonly EntityId[] },\n): EntityId[] {\n const exempt = new Set<EntityId>(matched)\n const pending = [...matched]\n // Visited-bounded, because the graph is walked by id: a cycle is\n // unrepresentable in a stored document, but this must not depend on that.\n while (pending.length > 0) {\n const id = pending.pop()\n if (id === undefined) continue\n for (const parent of scene.parentsOf(id)) {\n if (exempt.has(parent)) continue\n exempt.add(parent)\n pending.push(parent)\n }\n }\n return [...new Set(answered)].filter((id) => !exempt.has(id))\n}\n\n/** `answerEntityIds` order, each id once — the reporting order RFC 0004 fixes. */\nfunction distinctIds(candidates: readonly GoalCandidate[]): string[] {\n return [...new Set(candidates.map((candidate) => candidate.id))]\n}\n","import type { DetailedGeometryFeedback, GeometryValidationFeedback } from './types'\nimport type { GeometryExerciseWarning } from './getGeometryExerciseWarnings'\nimport { fingerprintGeometryAttempt, fingerprintGeometryExercise } from './gradingFingerprints'\nimport { loadExerciseDocument } from './exerciseLoad'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\nimport { evaluateGeometryState, evaluateGeometryStateDetailed } from './validateGeometryState'\nimport { validateGeometryStateInput } from './validateGeometryStateInput'\n\nexport interface CheckGeometryAnswerIssue extends GeometryExerciseValidationIssue {\n source: 'exercise' | 'geometry'\n}\n\n/**\n * Which grading inputs a successful check described (RFC 0005 § Decision 2).\n * Both are opaque and engine-produced: equal for equal normalized content,\n * whatever the object identity or key order, and the exercise member ignores\n * the authored solution. A host passes the whole result on and never computes\n * or compares these itself.\n */\nexport interface CheckGeometryAnswerFingerprints {\n exercise: string\n attempt: string\n}\n\nexport type CheckGeometryAnswerResult =\n | {\n success: true\n data: GeometryValidationFeedback\n fingerprints: CheckGeometryAnswerFingerprints\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: CheckGeometryAnswerIssue[]\n warnings: []\n }\n\n/**\n * Safe public Check entry point for untrusted application data. Structure is\n * validated first; valid geometry is then evaluated by the JSXGraph-backed\n * goal handlers used by validateGeometryState().\n */\nexport function checkGeometryAnswer(\n exerciseInput: unknown,\n geometryInput: unknown,\n): CheckGeometryAnswerResult {\n // The solution-free load: grading ignores the authored solution, so a\n // malformed one cannot fail a check (ADR 0015).\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return {\n success: false,\n data: null,\n errors: exercise.errors.map((issue) => ({ ...issue, source: 'exercise' })),\n warnings: [],\n }\n }\n\n const geometry = validateGeometryStateInput(geometryInput, exercise.data)\n if (!geometry.success) {\n return {\n success: false,\n data: null,\n errors: geometry.errors.map((issue) => ({ ...issue, source: 'geometry' })),\n warnings: [],\n }\n }\n\n return {\n success: true,\n data: evaluateGeometryState(exercise.data, geometry.data),\n fingerprints: {\n exercise: fingerprintGeometryExercise(exercise.data),\n attempt: fingerprintGeometryAttempt(geometry.data),\n },\n errors: [],\n warnings: exercise.warnings,\n }\n}\n\n/**\n * Which document was rejected, as a code rather than a sentence. `message`\n * stays the schema's human-readable detail and is never a reason code: a\n * malformed document is not a student's wrong answer, and nothing here reaches\n * geometry evaluation (RFC 0004 § Decision 5).\n */\nexport type CheckGeometryAnswerErrorCode = 'invalid-exercise' | 'invalid-attempt'\n\nexport interface DetailedCheckGeometryAnswerIssue extends CheckGeometryAnswerIssue {\n code: CheckGeometryAnswerErrorCode\n}\n\nexport type DetailedCheckGeometryAnswerResult =\n | {\n success: true\n data: DetailedGeometryFeedback\n fingerprints: CheckGeometryAnswerFingerprints\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: DetailedCheckGeometryAnswerIssue[]\n warnings: []\n }\n\n/**\n * `checkGeometryAnswer()` with the structured report: same untrusted inputs,\n * same success discriminant, fingerprints and warnings, detailed data on\n * success.\n */\nexport function checkGeometryAnswerDetailed(\n exerciseInput: unknown,\n geometryInput: unknown,\n): DetailedCheckGeometryAnswerResult {\n // The solution-free load: grading ignores the authored solution, so a\n // malformed one cannot fail a check (ADR 0015).\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return {\n success: false,\n data: null,\n errors: exercise.errors.map((issue) => ({\n ...issue,\n source: 'exercise',\n code: 'invalid-exercise',\n })),\n warnings: [],\n }\n }\n\n const geometry = validateGeometryStateInput(geometryInput, exercise.data)\n if (!geometry.success) {\n return {\n success: false,\n data: null,\n errors: geometry.errors.map((issue) => ({\n ...issue,\n source: 'geometry',\n code: 'invalid-attempt',\n })),\n warnings: [],\n }\n }\n\n return {\n success: true,\n data: evaluateGeometryStateDetailed(exercise.data, geometry.data),\n fingerprints: {\n exercise: fingerprintGeometryExercise(exercise.data),\n attempt: fingerprintGeometryAttempt(geometry.data),\n },\n errors: [],\n warnings: exercise.warnings,\n }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { resolveExercisePoints } from '../state/resolvePoints'\nimport { revealedAnswerFor } from './goals'\nimport type { RevealedAnswer } from './types'\n\nexport function revealGeometryAnswers(exercise: GeometryExercise): RevealedAnswer[] {\n // The exercise's own points, resolved: a canonical answer is measured\n // against the givens and nothing else, so there is no attempt to index here.\n const points = resolveExercisePoints(exercise)\n\n return exercise.goals.map((goal) => ({\n id: goal.id,\n shape: revealedAnswerFor(goal, { exercise, points }),\n }))\n}\n","import type { GeometryState } from '../schemas/geometryState.schema'\nimport { loadExerciseDocument, validateEmbeddedSolution } from './exerciseLoad'\nimport { validateGeometryState } from './validateGeometryState'\n\n/**\n * Why no canonical attempt came back, as a code rather than a sentence (RFC\n * 0005 § Decision 4). The three are kept apart because a host acts on them\n * differently: a broken exercise is the whole widget's problem, a missing or\n * malformed solution is an authoring gap, and an incorrect one means the\n * authored answer and the goals disagree.\n */\nexport type RevealGeometryAttemptErrorCode =\n 'invalid-exercise' | 'invalid-canonical-attempt' | 'canonical-attempt-incorrect'\n\nexport interface RevealGeometryAttemptIssue {\n code: RevealGeometryAttemptErrorCode\n path: Array<string | number>\n message: string\n}\n\nexport type RevealGeometryAttemptResult =\n | { success: true; data: GeometryState; errors: [] }\n | { success: false; data: null; errors: RevealGeometryAttemptIssue[] }\n\nfunction refused(errors: RevealGeometryAttemptIssue[]): RevealGeometryAttemptResult {\n return { success: false, data: null, errors }\n}\n\n/**\n * The authored canonical attempt an exercise embeds, for a host's Reveal\n * (ADR 0015). Synchronous and DOM-free, and safe on untrusted input: the\n * exercise is migrated and parsed, then its `solution` is migrated, parsed and\n * normalized as an attempt against that exercise, then graded under the\n * exercise's own policy. Only a solution that grades correct is returned.\n *\n * Nothing is synthesized from goals and nothing is merged with student work:\n * the host replaces its attempt value with `data`. Paths of a malformed\n * solution are rooted at `solution`, so they point into the exercise document\n * the host holds.\n */\nexport function revealGeometryAttempt(exerciseInput: unknown): RevealGeometryAttemptResult {\n // The same two stages `validateGeometryExercise()` runs, kept apart here only\n // so each failure gets its own code.\n const exercise = loadExerciseDocument(exerciseInput)\n if (!exercise.success) {\n return refused(exercise.errors.map((issue) => ({ code: 'invalid-exercise', ...issue })))\n }\n\n if (exercise.solution === undefined) {\n return refused([\n {\n code: 'invalid-canonical-attempt',\n path: ['solution'],\n message: 'this exercise has no canonical solution',\n },\n ])\n }\n\n const attempt = validateEmbeddedSolution(exercise.solution, exercise.data)\n if (!attempt.success) {\n return refused(attempt.errors.map((issue) => ({ code: 'invalid-canonical-attempt', ...issue })))\n }\n\n if (validateGeometryState(exercise.data, attempt.data).status !== 'correct') {\n return refused([\n {\n code: 'canonical-attempt-incorrect',\n path: ['solution'],\n message: 'the canonical solution does not satisfy this exercise',\n },\n ])\n }\n\n return { success: true, data: attempt.data, errors: [] }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { loadExerciseDocument, validateEmbeddedSolution } from './exerciseLoad'\nimport type { GeometryExerciseWarning } from './getGeometryExerciseWarnings'\n\nexport interface GeometryExerciseValidationIssue {\n path: Array<string | number>\n message: string\n}\n\nexport type GeometryExerciseValidationResult =\n | {\n success: true\n data: GeometryExercise\n errors: []\n warnings: GeometryExerciseWarning[]\n }\n | {\n success: false\n data: null\n errors: GeometryExerciseValidationIssue[]\n warnings: []\n }\n\n/**\n * The whole stored exercise, authored solution included. Stored exercises are\n * brought up to the current version before their structure is checked, exactly\n * as attempts are (M2.6.b), and an embedded `solution` is then migrated and\n * checked against the exercise as an attempt (ADR 0015), with its issues rooted\n * at `solution`. On success `data.solution` is that normalized attempt.\n *\n * Grading does not go through here: Check and the student component load the\n * solution-free projection, so a broken solution never disables them. An\n * authoring tool does, and learns about it at save time.\n */\nexport function validateGeometryExercise(input: unknown): GeometryExerciseValidationResult {\n const loaded = loadExerciseDocument(input)\n if (!loaded.success) {\n return { success: false, data: null, errors: loaded.errors, warnings: [] }\n }\n if (loaded.solution === undefined) {\n return { success: true, data: loaded.data, errors: [], warnings: loaded.warnings }\n }\n\n const solution = validateEmbeddedSolution(loaded.solution, loaded.data)\n if (!solution.success) {\n return { success: false, data: null, errors: solution.errors, warnings: [] }\n }\n return {\n success: true,\n data: { ...loaded.data, solution: solution.data },\n errors: [],\n warnings: loaded.warnings,\n }\n}\n\nexport function validateGeometryExerciseJson(json: string): GeometryExerciseValidationResult {\n try {\n return validateGeometryExercise(JSON.parse(json))\n } catch (error) {\n return {\n success: false,\n data: null,\n errors: [\n {\n path: [],\n message: error instanceof Error ? error.message : 'Invalid JSON syntax',\n },\n ],\n warnings: [],\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mathscan/geometry-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A React and TypeScript engine for creating, rendering, and validating interactive geometry exercises using Konva and JSXGraph",
|
|
5
5
|
"author": "mathscan",
|
|
6
6
|
"license": "mathscan-license",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./geometryState.schema-DKQ-durx.cjs");let t=require("zod");var n=t.z.number().finite().min(-100).max(100),r=t.z.tuple([n,n,n,n]).refine(([e,t,n,r])=>e<n&&t<r,{message:`viewport must satisfy xMin < xMax and yMin < yMax`}).refine(([e,,t])=>t-e>=1&&t-e<=100,{message:`viewport width must be between 1 and 100`}).refine(([,e,,t])=>t-e>=1&&t-e<=100,{message:`viewport height must be between 1 and 100`}),i=t.z.discriminatedUnion(`type`,[e.y,e.m,e.g,e._,e.h,e.v]),a=t.z.string().min(1).refine(e=>e===e.trim()),o=t.z.object({x:t.z.number().finite(),y:t.z.number().finite()}).strict(),s=t.z.tuple([a,a]).readonly(),c=t.z.number().finite().min(-100).max(100),l=t.z.number().positive().finite(),u=t.z.tuple([c,c,c,c]).refine(([e,t,n,r])=>e<n&&t<r).refine(([e,,t])=>t-e>=1&&t-e<=100).refine(([,e,,t])=>t-e>=1&&t-e<=100),d=[`point`,`segment`,`line`,`circle`],f=[`ruler`,`setSquare`,`protractor`],p=[`create`,`throughPoints`,`perpendicular`,`parallel`,`segmentLength`,`midpoint`],m=t.z.enum(d),h=t.z.number().positive().max(90),g=t.z.union([t.z.object({kind:t.z.literal(`point`),at:o}).strict(),t.z.object({kind:t.z.literal(`point`),on:a,t:t.z.number().finite()}).strict(),t.z.object({kind:t.z.literal(`point`),intersectionOf:s,which:t.z.union([t.z.literal(0),t.z.literal(1)]).optional()}).strict(),t.z.object({kind:t.z.literal(`segment`),parents:s}).strict(),t.z.object({kind:t.z.literal(`line`),parents:s}).strict(),t.z.object({kind:t.z.literal(`circle`),parents:s}).strict()]),_={create:{objectType:m},throughPoints:{objectType:m,pointIds:t.z.array(a).min(1)},perpendicular:{objectType:m,referenceObjectId:a,throughPointId:a,toleranceDegrees:h},parallel:{objectType:m,referenceObjectId:a,throughPointId:a,toleranceDegrees:h},segmentLength:{objectType:t.z.literal(`segment`),targetLength:l,toleranceUnits:l},midpoint:{objectType:t.z.literal(`point`),referenceObjectId:a,toleranceUnits:l}},v=t.z.union(p.map(e=>t.z.object({id:a,type:t.z.literal(e),..._[e]}).strict())),y=t.z.object({schemaVersion:t.z.literal(2),id:a,viewport:u,showGrid:t.z.boolean().optional(),entities:e.T(g),presentation:e.T(t.z.object({role:t.z.enum([`implicit`,`explicit`]),label:t.z.string().min(1).optional()}).strict()),draggableEntityIds:t.z.array(a),allowedObjects:t.z.array(m),tools:t.z.array(t.z.enum(f)),goals:t.z.array(v)}).strict();function b(e){let t=e.flatMap(e=>e.code===`unrecognized_keys`&&e.keys?e.keys.map(t=>[...e.path,t].join(`.`)):[e.path.join(`.`)||`the document`]);return[...new Set(t)]}function x(e,t,n){let r=e+1;return{from:e,to:r,migrate:i=>{let a=t.safeParse(i);if(!a.success&&n.safeParse({...i,schemaVersion:r}).success)throw Error(`a version ${e} exercise cannot carry ${b(a.error.issues).join(`, `)}`);return{...i}}}}function S(e){return x(2,y,e)}var C=y.extend({schemaVersion:t.z.literal(3),grading:t.z.object({extraObjects:t.z.enum([`allow`,`reject`])}).strict().optional()});function w(e){return x(3,C,e)}var T=t.z.enum([`ruler`,`setSquare`,`protractor`]),E=4,D=t.z.enum([`allow`,`reject`]),O=t.z.object({extraObjects:D}).strict(),k=t.z.object({schemaVersion:t.z.literal(4),id:e.D,viewport:r,showGrid:t.z.boolean().optional(),entities:e.T(e.c),presentation:e.T(e.s),draggableEntityIds:t.z.array(e.D),allowedObjects:t.z.array(e.b),tools:t.z.array(T),goals:t.z.array(i),grading:O.optional(),solution:e.n.optional()}).strict().superRefine((t,n)=>{for(let r of e.C(t,{parentsMustBeDeclared:!0}))n.addIssue({code:`custom`,path:r.path,message:r.message});for(let r of e.w(t.draggableEntityIds,`draggableEntityIds`,t,{pointsOnly:!0}))n.addIssue({code:`custom`,path:r.path,message:r.message});let r={definitionOf:n=>e.O(t.entities,n)},i=new Set;t.goals.forEach((t,a)=>{i.has(t.id)&&n.addIssue({code:`custom`,path:[`goals`,a,`id`],message:`duplicate goal id: ${t.id}`}),i.add(t.id);for(let i of e.d(t).crossReferenceIssues(t,r))n.addIssue({code:`custom`,path:[`goals`,a,...i.path],message:i.message})})}),A=[e.a,S(C),w({safeParse:t=>k.safeParse(typeof t==`object`&&t?e.u(t):t)})];function j(e){return k.parse(e)}function M(t){return e.o(t,A,4)}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return k}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return A}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return r}});
|
|
2
|
-
//# sourceMappingURL=exercise.schema-BV2b7rtN.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"exercise.schema-BV2b7rtN.cjs","names":[],"sources":["../src/schemas/viewport.schema.ts","../src/schemas/goals/geometryGoal.schema.ts","../src/schemas/exerciseV2.schema.ts","../src/schemas/v2ToV3.ts","../src/schemas/exerciseV3.schema.ts","../src/schemas/v3ToV4.ts","../src/schemas/exercise.schema.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst viewportCoordinateSchema = z.number().finite().min(-100).max(100)\n\nexport const mathViewportSchema = z\n .tuple([\n viewportCoordinateSchema,\n viewportCoordinateSchema,\n viewportCoordinateSchema,\n viewportCoordinateSchema,\n ])\n .refine(([xMin, yMin, xMax, yMax]) => xMin < xMax && yMin < yMax, {\n message: 'viewport must satisfy xMin < xMax and yMin < yMax',\n })\n .refine(([xMin, , xMax]) => xMax - xMin >= 1 && xMax - xMin <= 100, {\n message: 'viewport width must be between 1 and 100',\n })\n .refine(([, yMin, , yMax]) => yMax - yMin >= 1 && yMax - yMin <= 100, {\n message: 'viewport height must be between 1 and 100',\n })\n\nexport type MathViewport = z.infer<typeof mathViewportSchema>\n","import { z } from 'zod'\nimport { createGoalSchema } from './createGoal.schema'\nimport { throughPointsGoalSchema } from './throughPointsGoal.schema'\nimport { perpendicularGoalSchema } from './perpendicularGoal.schema'\nimport { parallelGoalSchema } from './parallelGoal.schema'\nimport { segmentLengthGoalSchema } from './segmentLengthGoal.schema'\nimport { midpointGoalSchema } from './midpointGoal.schema'\n\nexport const geometryGoalSchema = z.discriminatedUnion('type', [\n createGoalSchema,\n throughPointsGoalSchema,\n perpendicularGoalSchema,\n parallelGoalSchema,\n segmentLengthGoalSchema,\n midpointGoalSchema,\n])\n\nexport type GeometryGoal = z.infer<typeof geometryGoalSchema>\n","import { z } from 'zod'\nimport { entityRecordSchema } from './entityDocument'\n\nexport const v2ExerciseSchemaVersion = 2\n\n/**\n * The exercise document ADR 0008 froze, transcribed **whole** so the v2 → v3\n * step can check what it was handed before promoting it (ADR 0013).\n *\n * Frozen means frozen: nothing below is imported from the current vocabulary.\n * An earlier version of this file reused `entityDefinitionSchema`,\n * `entityPresentationSchema`, `objectTypeNameSchema`, `toolNameSchema` and\n * `geometryGoalSchema`, which would have silently admitted every later addition\n * to them — the `angle` entity kind, `angleMeasure` / `angleBisector` goals and\n * the `measureLabel` presentation entry ADR 0010 already plans — into a document\n * claiming to be version 2. A union member or an enum value is as much a version\n * change as a new field, and a shared schema cannot tell two versions apart.\n *\n * What is deliberately shared is machinery rather than vocabulary:\n * `entityRecordSchema()` supplies the `__proto__` refusal (M2.G.1) and the\n * record's id-key rule. If those change they change for every version at once,\n * which is a decision of its own rather than a new document feature.\n *\n * Two things this schema is not. It carries **no cross-reference refinements**:\n * it answers \"could version 2 have said this?\", never \"is this exercise\n * coherent?\" — the migrated document is parsed by `geometryExerciseSchema`,\n * whose refinements report a dangling id or a duplicate goal id at its real\n * path. And it carries **no issue messages**: nothing renders them, because\n * `v2ToV3.ts` reports the offending *paths* and the surviving document is\n * described by the current schema.\n *\n * `tests/schemas/v2ToV3.test.ts` § \"the frozen v2 vocabulary\" fails the day the\n * current schemas gain a kind, goal, object type, tool or presentation field\n * this file does not list, so the freeze cannot rot quietly.\n */\n\nconst id = z\n .string()\n .min(1)\n .refine((value) => value === value.trim())\nconst point = z.object({ x: z.number().finite(), y: z.number().finite() }).strict()\nconst parents = z.tuple([id, id]).readonly()\nconst span = z.number().finite().min(-100).max(100)\nconst positive = z.number().positive().finite()\n\nconst viewport = z\n .tuple([span, span, span, span])\n .refine(([xMin, yMin, xMax, yMax]) => xMin < xMax && yMin < yMax)\n .refine(([xMin, , xMax]) => xMax - xMin >= 1 && xMax - xMin <= 100)\n .refine(([, yMin, , yMax]) => yMax - yMin >= 1 && yMax - yMin <= 100)\n\n/** v2's four entity kinds, its object types, its tools and its goal types. */\nexport const v2EntityKinds = ['point', 'segment', 'line', 'circle'] as const\nexport const v2ObjectTypeNames = ['point', 'segment', 'line', 'circle'] as const\nexport const v2ToolNames = ['ruler', 'setSquare', 'protractor'] as const\nexport const v2GoalTypes = [\n 'create',\n 'throughPoints',\n 'perpendicular',\n 'parallel',\n 'segmentLength',\n 'midpoint',\n] as const\n/** And the only two keys a v2 presentation entry had. */\nexport const v2PresentationKeys = ['role', 'label'] as const\n\nconst objectType = z.enum(v2ObjectTypeNames)\nconst angle = z.number().positive().max(90)\n\nconst definition = z.union([\n z.object({ kind: z.literal('point'), at: point }).strict(),\n z.object({ kind: z.literal('point'), on: id, t: z.number().finite() }).strict(),\n z\n .object({\n kind: z.literal('point'),\n intersectionOf: parents,\n which: z.union([z.literal(0), z.literal(1)]).optional(),\n })\n .strict(),\n z.object({ kind: z.literal('segment'), parents }).strict(),\n z.object({ kind: z.literal('line'), parents }).strict(),\n z.object({ kind: z.literal('circle'), parents }).strict(),\n])\n\n// One row per v2 goal kind: the fields it carried beyond `id` and `type`.\nconst goalFields = {\n create: { objectType },\n throughPoints: { objectType, pointIds: z.array(id).min(1) },\n perpendicular: {\n objectType,\n referenceObjectId: id,\n throughPointId: id,\n toleranceDegrees: angle,\n },\n parallel: { objectType, referenceObjectId: id, throughPointId: id, toleranceDegrees: angle },\n segmentLength: {\n objectType: z.literal('segment'),\n targetLength: positive,\n toleranceUnits: positive,\n },\n midpoint: { objectType: z.literal('point'), referenceObjectId: id, toleranceUnits: positive },\n} as const\n\nconst goal = z.union(\n v2GoalTypes.map((type) => z.object({ id, type: z.literal(type), ...goalFields[type] }).strict()),\n)\n\nexport const geometryExerciseV2Schema = z\n .object({\n schemaVersion: z.literal(v2ExerciseSchemaVersion),\n id,\n viewport,\n showGrid: z.boolean().optional(),\n entities: entityRecordSchema(definition),\n presentation: entityRecordSchema(\n z\n .object({ role: z.enum(['implicit', 'explicit']), label: z.string().min(1).optional() })\n .strict(),\n ),\n draggableEntityIds: z.array(id),\n allowedObjects: z.array(objectType),\n tools: z.array(z.enum(v2ToolNames)),\n goals: z.array(goal),\n })\n .strict()\n","// The one-way v2 → v3 translation of a stored exercise (M3.H.6, ADR 0013).\n//\n// v3 adds one optional field, `grading`, and changes nothing else — so the\n// translation is the identity on every field a v2 document carries, and it\n// deliberately does **not** materialize a policy: absence means permissive,\n// and writing `{ extraObjects: 'allow' }` here would turn every historical\n// exercise into one that states a policy it never authored.\n//\n// The step does check its input, unlike `v1ToV2`'s translations, and for the\n// reason the human's version decision names: a v2 document carrying anything\n// version 2 could not say must be **rejected** rather than promoted into a\n// document where it is legal. Nothing downstream can catch that — after the hop\n// the vocabulary is the current one — so the gate has to be here. The v3 → v4\n// hop (ADR 0015) is the same kind of step, so both are built by one factory.\nimport type { ZodType } from 'zod'\nimport type { MigrationStep } from './migrate'\nimport { geometryExerciseV2Schema } from './exerciseV2.schema'\n\n/** The version the step below produces. */\nexport const v3SchemaVersion = 3\n\n/**\n * Just enough of a schema for the gate: the next version's parser, passed in\n * rather than imported to keep this module free of a cycle with\n * `exercise.schema.ts` — and small enough that a test can substitute a laxer\n * one and watch the gate react.\n */\nexport interface DocumentGate {\n safeParse(input: unknown): { success: boolean }\n}\n\ninterface FrozenIssue {\n code: string\n path: PropertyKey[]\n keys?: string[]\n}\n\n/** What the document said that the version it claims has no way to express. */\nfunction offendingPaths(issues: readonly FrozenIssue[]): string[] {\n const named = issues.flatMap((issue) =>\n issue.code === 'unrecognized_keys' && issue.keys\n ? issue.keys.map((key) => [...issue.path, key].join('.'))\n : [issue.path.join('.') || 'the document'],\n )\n return [...new Set(named)]\n}\n\n/**\n * An exercise hop that adds vocabulary and changes nothing it already had. The\n * refusal rule, stated exactly: **a document that is invalid at `from` and\n * valid at `from + 1` is refused.** That is what silent promotion means, and it\n * covers every way the next version can be wider — a new field, a new entity\n * kind, a new goal type, a new `allowedObjects` member, a new presentation\n * entry, a loosened value — without the step having to enumerate them or read\n * issue codes to guess which is which.\n *\n * A document that is invalid at **both** versions passes through untouched, so\n * an ordinary fault (a bad viewport, a dangling parent) is still reported by\n * `geometryExerciseSchema` at its own document path rather than refiled here as\n * one message under `schemaVersion`. It is rejected either way; only the quality\n * of the error differs, and the path is the better answer.\n *\n * `frozenSchema` must be a transcription of version `from` and `nextSchema` the\n * parser of version `from + 1` — frozen as well unless that is the current one,\n * because a current schema's version literal would never accept the promotion.\n */\nexport function createIdentityExerciseStep(\n from: number,\n frozenSchema: ZodType,\n nextSchema: DocumentGate,\n): MigrationStep {\n const to = from + 1\n return {\n from,\n to,\n migrate: (document) => {\n const parsed = frozenSchema.safeParse(document)\n if (!parsed.success && nextSchema.safeParse({ ...document, schemaVersion: to }).success) {\n throw new Error(\n `a version ${from} exercise cannot carry ${offendingPaths(parsed.error.issues).join(', ')}`,\n )\n }\n // The input document, not a parsed copy: the shape is unchanged, so the\n // stored values travel on untouched and `applyMigrations()` stamps the new\n // version.\n return { ...document }\n },\n }\n}\n\n/** The v2 → v3 hop, gated against the version 3 schema it is given. */\nexport function createGeometryExerciseV2ToV3(v3ExerciseSchema: DocumentGate): MigrationStep {\n return createIdentityExerciseStep(2, geometryExerciseV2Schema, v3ExerciseSchema)\n}\n","import { z } from 'zod'\nimport { geometryExerciseV2Schema } from './exerciseV2.schema'\n\nexport const v3ExerciseSchemaVersion = 3\n\n/**\n * The exercise document ADR 0013 froze at version 3, so the v3 → v4 step can\n * check what it was handed before promoting it (ADR 0015) — and so the v2 → v3\n * step still has a version 3 to ask once the current schema has moved past it.\n *\n * Version 3 is version 2 plus `grading` and nothing else, so it is built on the\n * frozen v2 transcription rather than transcribed a second time. `grading` is\n * written out here with nothing imported from the current vocabulary, for the\n * reason `exerciseV2.schema.ts` gives: a shared schema admits every later\n * addition to it. Like that schema this one carries no cross-reference\n * refinements and no issue messages; it only answers \"could version 3 have\n * said this?\".\n */\nexport const geometryExerciseV3Schema = geometryExerciseV2Schema.extend({\n schemaVersion: z.literal(v3ExerciseSchemaVersion),\n grading: z\n .object({ extraObjects: z.enum(['allow', 'reject']) })\n .strict()\n .optional(),\n})\n","// The one-way v3 → v4 translation of a stored exercise (M3.I.3, ADR 0015).\n//\n// v4 adds one optional field, `solution` — the authored canonical attempt — and\n// changes nothing else, so the translation is the identity plus a version and a\n// migrated v3 exercise simply has no solution. The gate is the v2 → v3 one a\n// version later: a v3 document carrying `solution`, or anything else version 3\n// could not say, is refused rather than promoted, because a v3 reader would\n// reject it too.\nimport type { MigrationStep } from './migrate'\nimport { geometryExerciseV3Schema } from './exerciseV3.schema'\nimport { createIdentityExerciseStep, type DocumentGate } from './v2ToV3'\n\nexport function createGeometryExerciseV3ToV4(currentExerciseSchema: DocumentGate): MigrationStep {\n return createIdentityExerciseStep(3, geometryExerciseV3Schema, currentExerciseSchema)\n}\n","import { z } from 'zod'\nimport { entryOf } from '../geometry/entityRecord'\nimport { geometryIdSchema } from './identifier.schema'\nimport { objectTypeNameSchema } from './objectTypeName.schema'\nimport { mathViewportSchema } from './viewport.schema'\nimport { entityDefinitionSchema } from './entityDefinition.schema'\nimport { entityPresentationSchema } from './entityPresentation.schema'\nimport { entityDocumentIssues, entityIdListIssues, entityRecordSchema } from './entityDocument'\nimport { geometryGoalSchema } from './goals/geometryGoal.schema'\nimport { declarationFor, type GoalAuthoringContext } from './goals/goalKindDeclaration'\nimport { applyMigrations, type MigrationOutcome, type MigrationStep } from './migrate'\nimport { geometryExerciseV1ToV2 } from './v1ToV2'\nimport { createGeometryExerciseV2ToV3 } from './v2ToV3'\nimport { createGeometryExerciseV3ToV4 } from './v3ToV4'\nimport { geometryExerciseV3Schema } from './exerciseV3.schema'\nimport { withoutSolution } from './exerciseProjection'\nimport { geometryStateSchema } from './geometryState.schema'\n\nexport const toolNameSchema = z.enum(['ruler', 'setSquare', 'protractor'])\nexport type ToolName = z.infer<typeof toolNameSchema>\n\n// An exercise is stored and reloaded exactly as an attempt is, so it versions\n// and migrates the same way (M2.6.b). Raised to 2 at M2.6.c.1, with the\n// consumers that read what the step below produces: a stored v1 exercise is now\n// migrated on the way in and no longer has a schema of its own, because nothing\n// parses a v1 exercise — `geometryExerciseV1ToV2` reads the raw JSON.\n//\n// Raised to 3 at M3.H.6 for `grading` (ADR 0013). The human rejected adding the\n// field to v2: an older reader claims to support version 2 and would reject the\n// document anyway, so the version says out loud what the field means. **The\n// attempt document stays at 2** — the two versions are independent, and a v2\n// attempt is graded against a v3 exercise.\n//\n// Raised to 4 at M3.I.3 for `solution` (ADR 0015), for the same reason: a v3\n// reader must not be handed a v3 document it would reject.\nexport const latestGeometryExerciseVersion = 4\n\n// How strictly an attempt is graded for answer entities no goal needed\n// (RFC 0004 § Decision 6). `allow` is what every exercise did before v3, so\n// **absence means `allow`** and nothing materializes a default: an exercise that\n// states no policy is indistinguishable from one migrated from v2.\nexport const extraObjectPolicySchema = z.enum(['allow', 'reject'])\nexport type ExtraObjectPolicy = z.infer<typeof extraObjectPolicySchema>\n\n// Strict and with `extraObjects` required when the object is present: a\n// `grading: {}` that silently meant \"allow\" would be a second way to say\n// nothing, and a misspelled key inside it would be read as permissive.\nexport const exerciseGradingSchema = z.object({ extraObjects: extraObjectPolicySchema }).strict()\nexport type ExerciseGrading = z.infer<typeof exerciseGradingSchema>\n\n// The exercise document ADR 0008 froze. `givens[]` became the same `entities`\n// record the attempt uses since M2.6.c.2, so a given segment and a student\n// segment are the same shape and only the document they live in distinguishes\n// them. `label` lives in the `presentation` record and `draggable` became\n// `draggableEntityIds` — a list on the exercise and unrepresentable in an\n// attempt, so a tampered attempt cannot declare a pinned given draggable and\n// move the reference its own goals are measured against.\n//\n// `id`, `viewport`, `showGrid`, `allowedObjects`, `tools` and `goals` are\n// unchanged from v1, and the goals array migrates by identity.\n//\n// v3 adds `grading` and nothing else. An attempt cannot carry it — the\n// strictness policy is the author's, not the student's — and it is optional, so\n// a migrated v2 exercise is a v3 exercise with the field absent.\nexport const geometryExerciseSchema = z\n .object({\n schemaVersion: z.literal(latestGeometryExerciseVersion),\n id: geometryIdSchema,\n viewport: mathViewportSchema,\n showGrid: z.boolean().optional(),\n entities: entityRecordSchema(entityDefinitionSchema),\n presentation: entityRecordSchema(entityPresentationSchema),\n draggableEntityIds: z.array(geometryIdSchema),\n allowedObjects: z.array(objectTypeNameSchema),\n tools: z.array(toolNameSchema),\n goals: z.array(geometryGoalSchema),\n grading: exerciseGradingSchema.optional(),\n // v4's authored canonical attempt (ADR 0015), a current-version attempt\n // document. The loaders never hand it to this schema raw: a stored solution\n // may be an older attempt version, so it is migrated and checked against the\n // exercise on its own (`validateEmbeddedSolution()`), and Check and the\n // student canvas load the solution-free projection instead, so a broken\n // authored answer never disables them (RFC 0005 § Decision 4, option C).\n solution: geometryStateSchema.optional(),\n })\n .strict()\n .superRefine((exercise, context) => {\n // An exercise is self-contained — nothing in it may name an id it does not\n // declare — which is the half of the staging an attempt cannot have.\n for (const issue of entityDocumentIssues(exercise, { parentsMustBeDeclared: true })) {\n context.addIssue({ code: 'custom', path: issue.path, message: issue.message })\n }\n // Only a point can be dragged: dragging a segment would have to decide what\n // happens to its endpoints, which is a command, not a stored capability.\n for (const issue of entityIdListIssues(\n exercise.draggableEntityIds,\n 'draggableEntityIds',\n exercise,\n { pointsOnly: true },\n )) {\n context.addIssue({ code: 'custom', path: issue.path, message: issue.message })\n }\n\n // Each goal kind's own cross-reference rules, from its declaration (M2.5),\n // so this refinement stays the exercise-wide part. The entity record *is*\n // the authoring context, so nothing translates.\n const authoring: GoalAuthoringContext = {\n definitionOf: (id) => entryOf(exercise.entities, id),\n }\n const goalIds = new Set<string>()\n exercise.goals.forEach((goal, index) => {\n if (goalIds.has(goal.id)) {\n context.addIssue({\n code: 'custom',\n path: ['goals', index, 'id'],\n message: `duplicate goal id: ${goal.id}`,\n })\n }\n goalIds.add(goal.id)\n for (const issue of declarationFor(goal).crossReferenceIssues(goal, authoring)) {\n context.addIssue({\n code: 'custom',\n path: ['goals', index, ...issue.path],\n message: issue.message,\n })\n }\n })\n })\n\nexport type GeometryExercise = z.infer<typeof geometryExerciseSchema>\n\n// One entry per version hop. Each gate asks the next version's schema whether\n// the document it is refusing would have been accepted there — the definition\n// of a silent promotion. For v2 → v3 that is the frozen v3 schema, because the\n// current one only accepts version 4; for v3 → v4 it is the current schema,\n// declared above, and passing it in keeps `v3ToV4.ts` out of an import cycle.\n//\n// The v3 → v4 gate asks without the solution: carrying `solution` at all is\n// what version 3 cannot say, whatever the solution contains. Asking with it\n// would let a v3 document with a *malformed* solution be invalid at both\n// versions, pass through, and then load cleanly on the solution-free path.\nexport const geometryExerciseMigrations: readonly MigrationStep[] = [\n geometryExerciseV1ToV2,\n createGeometryExerciseV2ToV3(geometryExerciseV3Schema),\n createGeometryExerciseV3ToV4({\n safeParse: (document) =>\n geometryExerciseSchema.safeParse(\n typeof document === 'object' && document !== null ? withoutSolution(document) : document,\n ),\n }),\n]\n\nexport function parseGeometryExercise(input: unknown): GeometryExercise {\n return geometryExerciseSchema.parse(input)\n}\n\n// Brings a stored exercise up to the current version, the counterpart of\n// `migrateGeometryState()`. Structure is checked afterwards by the schema; this\n// only moves the shape forward, and it never mutates its input.\nexport function migrateGeometryExercise(input: unknown): MigrationOutcome {\n return applyMigrations(input, geometryExerciseMigrations, latestGeometryExerciseVersion)\n}\n"],"mappings":"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"}
|