@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.
Files changed (238) hide show
  1. package/README.md +24 -7
  2. package/dist/core.cjs +1 -1
  3. package/dist/core.mjs +4 -4
  4. package/dist/entries/core.d.cts +1 -1
  5. package/dist/entries/core.d.ts +1 -1
  6. package/dist/entries/core.d.ts.map +1 -1
  7. package/dist/exercise.schema-Bma3qN9P.js +251 -0
  8. package/dist/exercise.schema-Bma3qN9P.js.map +1 -0
  9. package/dist/exercise.schema-Oj5ZH-yO.cjs +2 -0
  10. package/dist/exercise.schema-Oj5ZH-yO.cjs.map +1 -0
  11. package/dist/exerciseLoad-Db9Cigxi.cjs +2 -0
  12. package/dist/exerciseLoad-Db9Cigxi.cjs.map +1 -0
  13. package/dist/{geometryAttemptStatus-5L_q6-9l.js → exerciseLoad-DgeDXYbf.js} +84 -71
  14. package/dist/exerciseLoad-DgeDXYbf.js.map +1 -0
  15. package/dist/geometry/entities/angle.d.cts +56 -0
  16. package/dist/geometry/entities/angle.d.ts +57 -0
  17. package/dist/geometry/entities/angle.d.ts.map +1 -0
  18. package/dist/geometry/entities/circle.d.ts.map +1 -1
  19. package/dist/geometry/entities/index.d.cts +8 -4
  20. package/dist/geometry/entities/index.d.ts +8 -4
  21. package/dist/geometry/entities/index.d.ts.map +1 -1
  22. package/dist/geometry/entities/line.d.ts.map +1 -1
  23. package/dist/geometry/entities/point.d.ts.map +1 -1
  24. package/dist/geometry/entities/relations.d.cts +8 -1
  25. package/dist/geometry/entities/relations.d.ts +8 -1
  26. package/dist/geometry/entities/relations.d.ts.map +1 -1
  27. package/dist/geometry/entities/segment.d.ts.map +1 -1
  28. package/dist/geometry/entityModel.d.cts +21 -3
  29. package/dist/geometry/entityModel.d.ts +21 -3
  30. package/dist/geometry/entityModel.d.ts.map +1 -1
  31. package/dist/geometryState.schema-B5ccMKDa.cjs +2 -0
  32. package/dist/geometryState.schema-B5ccMKDa.cjs.map +1 -0
  33. package/dist/geometryState.schema-CKFPlX8j.js +1177 -0
  34. package/dist/geometryState.schema-CKFPlX8j.js.map +1 -0
  35. package/dist/goals-BlSeniyt.js +563 -0
  36. package/dist/goals-BlSeniyt.js.map +1 -0
  37. package/dist/goals-DLbmBZD8.cjs +2 -0
  38. package/dist/goals-DLbmBZD8.cjs.map +1 -0
  39. package/dist/index.cjs +1 -1
  40. package/dist/index.mjs +8 -8
  41. package/dist/measure/angleType.d.cts +2 -0
  42. package/dist/measure/angleType.d.ts +3 -0
  43. package/dist/measure/angleType.d.ts.map +1 -0
  44. package/dist/measure/geometryAdapter.d.cts +3 -0
  45. package/dist/measure/geometryAdapter.d.ts +3 -0
  46. package/dist/measure/geometryAdapter.d.ts.map +1 -1
  47. package/dist/react-B26_e52I.cjs +2 -0
  48. package/dist/react-B26_e52I.cjs.map +1 -0
  49. package/dist/{react-BEcdx1f6.js → react-B5cGaxH8.js} +738 -513
  50. package/dist/react-B5cGaxH8.js.map +1 -0
  51. package/dist/react.cjs +1 -1
  52. package/dist/react.mjs +1 -1
  53. package/dist/renderer/GeometryCanvas.d.ts.map +1 -1
  54. package/dist/renderer/StudentGeometryCanvas.d.ts.map +1 -1
  55. package/dist/renderer/Toolbar.d.ts.map +1 -1
  56. package/dist/renderer/angleMeasure.d.cts +14 -0
  57. package/dist/renderer/angleMeasure.d.ts +15 -0
  58. package/dist/renderer/angleMeasure.d.ts.map +1 -0
  59. package/dist/renderer/deriveRenderableGeometry.d.cts +1 -0
  60. package/dist/renderer/deriveRenderableGeometry.d.ts +1 -0
  61. package/dist/renderer/deriveRenderableGeometry.d.ts.map +1 -1
  62. package/dist/renderer/deriveSemanticScene.d.ts.map +1 -1
  63. package/dist/renderer/dragSnap.d.cts +14 -0
  64. package/dist/renderer/dragSnap.d.ts +15 -0
  65. package/dist/renderer/dragSnap.d.ts.map +1 -0
  66. package/dist/renderer/shapes/AngleShape.d.cts +19 -0
  67. package/dist/renderer/shapes/AngleShape.d.ts +20 -0
  68. package/dist/renderer/shapes/AngleShape.d.ts.map +1 -0
  69. package/dist/renderer/shapes/EntityLabel.d.cts +9 -1
  70. package/dist/renderer/shapes/EntityLabel.d.ts +9 -1
  71. package/dist/renderer/shapes/EntityLabel.d.ts.map +1 -1
  72. package/dist/renderer/shapes/EntityShape.d.cts +2 -1
  73. package/dist/renderer/shapes/EntityShape.d.ts +2 -1
  74. package/dist/renderer/shapes/EntityShape.d.ts.map +1 -1
  75. package/dist/renderer/useConnectionPreview.d.cts +12 -1
  76. package/dist/renderer/useConnectionPreview.d.ts +12 -1
  77. package/dist/renderer/useConnectionPreview.d.ts.map +1 -1
  78. package/dist/renderer/usePointerGestures.d.cts +1 -1
  79. package/dist/renderer/usePointerGestures.d.ts +1 -1
  80. package/dist/renderer/usePointerGestures.d.ts.map +1 -1
  81. package/dist/schemas/entityDefinition.schema.d.cts +7 -0
  82. package/dist/schemas/entityDefinition.schema.d.ts +7 -0
  83. package/dist/schemas/entityDefinition.schema.d.ts.map +1 -1
  84. package/dist/schemas/entityDocument.d.cts +34 -13
  85. package/dist/schemas/entityDocument.d.ts +34 -13
  86. package/dist/schemas/entityDocument.d.ts.map +1 -1
  87. package/dist/schemas/entityPresentation.schema.d.cts +3 -0
  88. package/dist/schemas/entityPresentation.schema.d.ts +3 -0
  89. package/dist/schemas/entityPresentation.schema.d.ts.map +1 -1
  90. package/dist/schemas/exercise.schema.d.cts +80 -2
  91. package/dist/schemas/exercise.schema.d.ts +80 -2
  92. package/dist/schemas/exercise.schema.d.ts.map +1 -1
  93. package/dist/schemas/exerciseV2.schema.d.cts +57 -0
  94. package/dist/schemas/exerciseV2.schema.d.ts +57 -0
  95. package/dist/schemas/exerciseV2.schema.d.ts.map +1 -1
  96. package/dist/schemas/exerciseV4.schema.d.cts +168 -0
  97. package/dist/schemas/exerciseV4.schema.d.ts +169 -0
  98. package/dist/schemas/exerciseV4.schema.d.ts.map +1 -0
  99. package/dist/schemas/geometryState.schema.d.cts +8 -0
  100. package/dist/schemas/geometryState.schema.d.ts +8 -0
  101. package/dist/schemas/geometryState.schema.d.ts.map +1 -1
  102. package/dist/schemas/goals/angleBisectorGoal.schema.d.cts +15 -0
  103. package/dist/schemas/goals/angleBisectorGoal.schema.d.ts +16 -0
  104. package/dist/schemas/goals/angleBisectorGoal.schema.d.ts.map +1 -0
  105. package/dist/schemas/goals/angleCriterion.schema.d.cts +28 -0
  106. package/dist/schemas/goals/angleCriterion.schema.d.ts +29 -0
  107. package/dist/schemas/goals/angleCriterion.schema.d.ts.map +1 -0
  108. package/dist/schemas/goals/angleMeasureGoal.schema.d.cts +26 -0
  109. package/dist/schemas/goals/angleMeasureGoal.schema.d.ts +27 -0
  110. package/dist/schemas/goals/angleMeasureGoal.schema.d.ts.map +1 -0
  111. package/dist/schemas/goals/createGoal.schema.d.cts +1 -0
  112. package/dist/schemas/goals/createGoal.schema.d.ts +1 -0
  113. package/dist/schemas/goals/createGoal.schema.d.ts.map +1 -1
  114. package/dist/schemas/goals/geometryGoal.schema.d.cts +61 -0
  115. package/dist/schemas/goals/geometryGoal.schema.d.ts +61 -0
  116. package/dist/schemas/goals/geometryGoal.schema.d.ts.map +1 -1
  117. package/dist/schemas/goals/givenAngleGoal.schema.d.cts +35 -0
  118. package/dist/schemas/goals/givenAngleGoal.schema.d.ts +36 -0
  119. package/dist/schemas/goals/givenAngleGoal.schema.d.ts.map +1 -0
  120. package/dist/schemas/goals/goalKindDeclaration.d.cts +7 -0
  121. package/dist/schemas/goals/goalKindDeclaration.d.ts +7 -0
  122. package/dist/schemas/goals/goalKindDeclaration.d.ts.map +1 -1
  123. package/dist/schemas/goals/parallelGoal.schema.d.cts +1 -0
  124. package/dist/schemas/goals/parallelGoal.schema.d.ts +1 -0
  125. package/dist/schemas/goals/parallelGoal.schema.d.ts.map +1 -1
  126. package/dist/schemas/goals/perpendicularGoal.schema.d.cts +1 -0
  127. package/dist/schemas/goals/perpendicularGoal.schema.d.ts +1 -0
  128. package/dist/schemas/goals/perpendicularGoal.schema.d.ts.map +1 -1
  129. package/dist/schemas/goals/throughPointsGoal.schema.d.cts +1 -0
  130. package/dist/schemas/goals/throughPointsGoal.schema.d.ts +1 -0
  131. package/dist/schemas/goals/throughPointsGoal.schema.d.ts.map +1 -1
  132. package/dist/schemas/objectTypeName.schema.d.cts +1 -0
  133. package/dist/schemas/objectTypeName.schema.d.ts +1 -0
  134. package/dist/schemas/objectTypeName.schema.d.ts.map +1 -1
  135. package/dist/schemas/v2ToV3.d.cts +15 -1
  136. package/dist/schemas/v2ToV3.d.ts +15 -1
  137. package/dist/schemas/v2ToV3.d.ts.map +1 -1
  138. package/dist/schemas/v3ToV4.d.cts +2 -1
  139. package/dist/schemas/v3ToV4.d.ts +2 -1
  140. package/dist/schemas/v3ToV4.d.ts.map +1 -1
  141. package/dist/schemas/v4ToV5.d.cts +7 -0
  142. package/dist/schemas/v4ToV5.d.ts +8 -0
  143. package/dist/schemas/v4ToV5.d.ts.map +1 -0
  144. package/dist/schemas.cjs +1 -1
  145. package/dist/schemas.mjs +3 -3
  146. package/dist/snapping/computeDragSnap.d.cts +30 -0
  147. package/dist/snapping/computeDragSnap.d.ts +31 -0
  148. package/dist/snapping/computeDragSnap.d.ts.map +1 -0
  149. package/dist/state/geometryContext.d.ts.map +1 -1
  150. package/dist/store/attemptChange.d.cts +1 -1
  151. package/dist/store/attemptChange.d.ts +1 -1
  152. package/dist/store/attemptChange.d.ts.map +1 -1
  153. package/dist/store/constructionCommand.d.cts +14 -2
  154. package/dist/store/constructionCommand.d.ts +14 -2
  155. package/dist/store/constructionCommand.d.ts.map +1 -1
  156. package/dist/store/constructionIdentity.d.cts +9 -0
  157. package/dist/store/constructionIdentity.d.ts +9 -0
  158. package/dist/store/constructionIdentity.d.ts.map +1 -1
  159. package/dist/store/studentStore.d.cts +7 -0
  160. package/dist/store/studentStore.d.ts +7 -0
  161. package/dist/store/studentStore.d.ts.map +1 -1
  162. package/dist/store/teacherStore.d.cts +16 -0
  163. package/dist/store/teacherStore.d.ts +16 -0
  164. package/dist/store/teacherStore.d.ts.map +1 -1
  165. package/dist/{studentStore-CEKTtMCh.js → studentStore-CyYa0N93.js} +502 -370
  166. package/dist/studentStore-CyYa0N93.js.map +1 -0
  167. package/dist/studentStore-ubldy4PJ.cjs +2 -0
  168. package/dist/studentStore-ubldy4PJ.cjs.map +1 -0
  169. package/dist/validation/getGeometryExerciseWarnings.d.cts +3 -2
  170. package/dist/validation/getGeometryExerciseWarnings.d.ts +3 -2
  171. package/dist/validation/getGeometryExerciseWarnings.d.ts.map +1 -1
  172. package/dist/validation/goals/angleBisector.d.cts +3 -0
  173. package/dist/validation/goals/angleBisector.d.ts +4 -0
  174. package/dist/validation/goals/angleBisector.d.ts.map +1 -0
  175. package/dist/validation/goals/angleCriterion.d.cts +12 -0
  176. package/dist/validation/goals/angleCriterion.d.ts +13 -0
  177. package/dist/validation/goals/angleCriterion.d.ts.map +1 -0
  178. package/dist/validation/goals/angleMeasure.d.cts +9 -0
  179. package/dist/validation/goals/angleMeasure.d.ts +10 -0
  180. package/dist/validation/goals/angleMeasure.d.ts.map +1 -0
  181. package/dist/validation/goals/create.d.cts +2 -2
  182. package/dist/validation/goals/create.d.ts +2 -2
  183. package/dist/validation/goals/create.d.ts.map +1 -1
  184. package/dist/validation/goals/givenAngle.d.cts +11 -0
  185. package/dist/validation/goals/givenAngle.d.ts +12 -0
  186. package/dist/validation/goals/givenAngle.d.ts.map +1 -0
  187. package/dist/validation/goals/goalKindHandler.d.cts +38 -1
  188. package/dist/validation/goals/goalKindHandler.d.ts +38 -1
  189. package/dist/validation/goals/goalKindHandler.d.ts.map +1 -1
  190. package/dist/validation/goals/index.d.cts +9 -2
  191. package/dist/validation/goals/index.d.ts +9 -2
  192. package/dist/validation/goals/index.d.ts.map +1 -1
  193. package/dist/validation/goals/midpoint.d.cts +2 -2
  194. package/dist/validation/goals/midpoint.d.ts +2 -2
  195. package/dist/validation/goals/midpoint.d.ts.map +1 -1
  196. package/dist/validation/goals/parallel.d.cts +2 -2
  197. package/dist/validation/goals/parallel.d.ts +2 -2
  198. package/dist/validation/goals/parallel.d.ts.map +1 -1
  199. package/dist/validation/goals/perpendicular.d.cts +2 -2
  200. package/dist/validation/goals/perpendicular.d.ts +2 -2
  201. package/dist/validation/goals/perpendicular.d.ts.map +1 -1
  202. package/dist/validation/goals/segmentLength.d.cts +2 -2
  203. package/dist/validation/goals/segmentLength.d.ts +2 -2
  204. package/dist/validation/goals/segmentLength.d.ts.map +1 -1
  205. package/dist/validation/goals/throughPoints.d.cts +2 -2
  206. package/dist/validation/goals/throughPoints.d.ts +2 -2
  207. package/dist/validation/goals/throughPoints.d.ts.map +1 -1
  208. package/dist/validation/types.d.cts +5 -1
  209. package/dist/validation/types.d.ts +5 -1
  210. package/dist/validation/types.d.ts.map +1 -1
  211. package/dist/validation/validateGeometryState.d.ts.map +1 -1
  212. package/dist/validation/validateGeometryStateInput.d.ts.map +1 -1
  213. package/dist/validation.cjs +1 -1
  214. package/dist/validation.cjs.map +1 -1
  215. package/dist/validation.mjs +106 -85
  216. package/dist/validation.mjs.map +1 -1
  217. package/package.json +1 -1
  218. package/dist/exercise.schema-BV2b7rtN.cjs +0 -2
  219. package/dist/exercise.schema-BV2b7rtN.cjs.map +0 -1
  220. package/dist/exercise.schema-ckVBW9XA.js +0 -212
  221. package/dist/exercise.schema-ckVBW9XA.js.map +0 -1
  222. package/dist/geometryAttemptStatus-5L_q6-9l.js.map +0 -1
  223. package/dist/geometryAttemptStatus-N_XHIkiS.cjs +0 -2
  224. package/dist/geometryAttemptStatus-N_XHIkiS.cjs.map +0 -1
  225. package/dist/geometryState.schema-D0ixyGYE.js +0 -955
  226. package/dist/geometryState.schema-D0ixyGYE.js.map +0 -1
  227. package/dist/geometryState.schema-DKQ-durx.cjs +0 -2
  228. package/dist/geometryState.schema-DKQ-durx.cjs.map +0 -1
  229. package/dist/goals-C3ZwbEh_.js +0 -451
  230. package/dist/goals-C3ZwbEh_.js.map +0 -1
  231. package/dist/goals-CxR5qPDC.cjs +0 -2
  232. package/dist/goals-CxR5qPDC.cjs.map +0 -1
  233. package/dist/react-BEcdx1f6.js.map +0 -1
  234. package/dist/react-Pp_VSk--.cjs +0 -2
  235. package/dist/react-Pp_VSk--.cjs.map +0 -1
  236. package/dist/studentStore-CEKTtMCh.js.map +0 -1
  237. package/dist/studentStore-DLftfFAl.cjs +0 -2
  238. package/dist/studentStore-DLftfFAl.cjs.map +0 -1
@@ -1,212 +0,0 @@
1
- import { C as e, D as t, O as n, T as r, _ as i, a, b as o, c as s, d as c, g as l, h as u, m as d, n as f, o as p, s as m, u as h, v as g, w as _, y as v } from "./geometryState.schema-D0ixyGYE.js";
2
- import { z as y } from "zod";
3
- //#region src/schemas/viewport.schema.ts
4
- var b = y.number().finite().min(-100).max(100), x = y.tuple([
5
- b,
6
- b,
7
- b,
8
- b
9
- ]).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" }), S = y.discriminatedUnion("type", [
10
- v,
11
- d,
12
- l,
13
- i,
14
- u,
15
- g
16
- ]), C = y.string().min(1).refine((e) => e === e.trim()), w = y.object({
17
- x: y.number().finite(),
18
- y: y.number().finite()
19
- }).strict(), T = y.tuple([C, C]).readonly(), E = y.number().finite().min(-100).max(100), D = y.number().positive().finite(), O = y.tuple([
20
- E,
21
- E,
22
- E,
23
- E
24
- ]).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), k = [
25
- "point",
26
- "segment",
27
- "line",
28
- "circle"
29
- ], A = [
30
- "ruler",
31
- "setSquare",
32
- "protractor"
33
- ], j = [
34
- "create",
35
- "throughPoints",
36
- "perpendicular",
37
- "parallel",
38
- "segmentLength",
39
- "midpoint"
40
- ], M = y.enum(k), N = y.number().positive().max(90), P = y.union([
41
- y.object({
42
- kind: y.literal("point"),
43
- at: w
44
- }).strict(),
45
- y.object({
46
- kind: y.literal("point"),
47
- on: C,
48
- t: y.number().finite()
49
- }).strict(),
50
- y.object({
51
- kind: y.literal("point"),
52
- intersectionOf: T,
53
- which: y.union([y.literal(0), y.literal(1)]).optional()
54
- }).strict(),
55
- y.object({
56
- kind: y.literal("segment"),
57
- parents: T
58
- }).strict(),
59
- y.object({
60
- kind: y.literal("line"),
61
- parents: T
62
- }).strict(),
63
- y.object({
64
- kind: y.literal("circle"),
65
- parents: T
66
- }).strict()
67
- ]), F = {
68
- create: { objectType: M },
69
- throughPoints: {
70
- objectType: M,
71
- pointIds: y.array(C).min(1)
72
- },
73
- perpendicular: {
74
- objectType: M,
75
- referenceObjectId: C,
76
- throughPointId: C,
77
- toleranceDegrees: N
78
- },
79
- parallel: {
80
- objectType: M,
81
- referenceObjectId: C,
82
- throughPointId: C,
83
- toleranceDegrees: N
84
- },
85
- segmentLength: {
86
- objectType: y.literal("segment"),
87
- targetLength: D,
88
- toleranceUnits: D
89
- },
90
- midpoint: {
91
- objectType: y.literal("point"),
92
- referenceObjectId: C,
93
- toleranceUnits: D
94
- }
95
- }, I = y.union(j.map((e) => y.object({
96
- id: C,
97
- type: y.literal(e),
98
- ...F[e]
99
- }).strict())), L = y.object({
100
- schemaVersion: y.literal(2),
101
- id: C,
102
- viewport: O,
103
- showGrid: y.boolean().optional(),
104
- entities: r(P),
105
- presentation: r(y.object({
106
- role: y.enum(["implicit", "explicit"]),
107
- label: y.string().min(1).optional()
108
- }).strict()),
109
- draggableEntityIds: y.array(C),
110
- allowedObjects: y.array(M),
111
- tools: y.array(y.enum(A)),
112
- goals: y.array(I)
113
- }).strict();
114
- //#endregion
115
- //#region src/schemas/v2ToV3.ts
116
- function R(e) {
117
- let t = e.flatMap((e) => e.code === "unrecognized_keys" && e.keys ? e.keys.map((t) => [...e.path, t].join(".")) : [e.path.join(".") || "the document"]);
118
- return [...new Set(t)];
119
- }
120
- function z(e, t, n) {
121
- let r = e + 1;
122
- return {
123
- from: e,
124
- to: r,
125
- migrate: (i) => {
126
- let a = t.safeParse(i);
127
- if (!a.success && n.safeParse({
128
- ...i,
129
- schemaVersion: r
130
- }).success) throw Error(`a version ${e} exercise cannot carry ${R(a.error.issues).join(", ")}`);
131
- return { ...i };
132
- }
133
- };
134
- }
135
- function B(e) {
136
- return z(2, L, e);
137
- }
138
- var V = L.extend({
139
- schemaVersion: y.literal(3),
140
- grading: y.object({ extraObjects: y.enum(["allow", "reject"]) }).strict().optional()
141
- });
142
- //#endregion
143
- //#region src/schemas/v3ToV4.ts
144
- function H(e) {
145
- return z(3, V, e);
146
- }
147
- //#endregion
148
- //#region src/schemas/exercise.schema.ts
149
- var U = y.enum([
150
- "ruler",
151
- "setSquare",
152
- "protractor"
153
- ]), W = 4, G = y.enum(["allow", "reject"]), K = y.object({ extraObjects: G }).strict(), q = y.object({
154
- schemaVersion: y.literal(4),
155
- id: t,
156
- viewport: x,
157
- showGrid: y.boolean().optional(),
158
- entities: r(s),
159
- presentation: r(m),
160
- draggableEntityIds: y.array(t),
161
- allowedObjects: y.array(o),
162
- tools: y.array(U),
163
- goals: y.array(S),
164
- grading: K.optional(),
165
- solution: f.optional()
166
- }).strict().superRefine((t, r) => {
167
- for (let n of e(t, { parentsMustBeDeclared: !0 })) r.addIssue({
168
- code: "custom",
169
- path: n.path,
170
- message: n.message
171
- });
172
- for (let e of _(t.draggableEntityIds, "draggableEntityIds", t, { pointsOnly: !0 })) r.addIssue({
173
- code: "custom",
174
- path: e.path,
175
- message: e.message
176
- });
177
- let i = { definitionOf: (e) => n(t.entities, e) }, a = /* @__PURE__ */ new Set();
178
- t.goals.forEach((e, t) => {
179
- a.has(e.id) && r.addIssue({
180
- code: "custom",
181
- path: [
182
- "goals",
183
- t,
184
- "id"
185
- ],
186
- message: `duplicate goal id: ${e.id}`
187
- }), a.add(e.id);
188
- for (let n of c(e).crossReferenceIssues(e, i)) r.addIssue({
189
- code: "custom",
190
- path: [
191
- "goals",
192
- t,
193
- ...n.path
194
- ],
195
- message: n.message
196
- });
197
- });
198
- }), J = [
199
- a,
200
- B(V),
201
- H({ safeParse: (e) => q.safeParse(typeof e == "object" && e ? h(e) : e) })
202
- ];
203
- function Y(e) {
204
- return q.parse(e);
205
- }
206
- function X(e) {
207
- return p(e, J, 4);
208
- }
209
- //#endregion
210
- export { W as a, U as c, q as i, S as l, G as n, X as o, J as r, Y as s, K as t, x as u };
211
-
212
- //# sourceMappingURL=exercise.schema-ckVBW9XA.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"exercise.schema-ckVBW9XA.js","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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"geometryAttemptStatus-5L_q6-9l.js","names":[],"sources":["../src/validation/validateGeometryStateInput.ts","../src/validation/deriveGeometryCanvasFeedback.ts","../src/validation/gradingFingerprints.ts","../src/validation/getGeometryExerciseWarnings.ts","../src/validation/exerciseLoad.ts","../src/validation/geometryAttemptStatus.ts"],"sourcesContent":["import type { EntityId } from '../geometry/entityModel'\nimport { entryOf } from '../geometry/entityRecord'\nimport {\n crossDocumentIdCollisionIssues,\n crossDocumentParentIssues,\n} from '../schemas/entityDocument'\nimport type { GeometryExercise } from '../schemas/exercise.schema'\nimport {\n geometryStateSchema,\n migrateGeometryState,\n type GeometryState,\n} from '../schemas/geometryState.schema'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\n\n/**\n * The load pass ADR 0008 § Amendment specifies: a v2 attempt carries **only\n * student work**, so an entity re-declaring a *pinned* given point is removed\n * on the way in, and no given at all survives in `answerEntityIds`.\n *\n * Neither drop is an error, because both are what every migrated v1 attempt\n * looks like — v1 seeded the givens into the attempt, required them to be\n * there, and its reveal promoted a given point into `constructedPointIds`,\n * which the migration copies verbatim into `answerEntityIds`. A *draggable*\n * given entity is kept: re-declaring one is how the student moves it.\n *\n * A dropped entity's `presentation` entry goes with it, both because a\n * leftover would make the normalized document fail the schema it just passed,\n * and because it would be saying something about an entity this document no\n * longer declares.\n *\n * **`answerEntityIds` loses every id the exercise declares**, not only the\n * dropped ones: a given is not an answer to anything, so a draggable given the\n * student moved is still not something they built. Dropping rather than\n * refusing is decided by what the loader can see — a hand-written document\n * naming a given there is indistinguishable from a migrated v1 one, and\n * refusing would make every stored v1 attempt with a revealed given\n * unloadable. The entry says nothing true either way, so it is removed in\n * silence, exactly as the pinned re-declaration is.\n */\nfunction normalizedAgainstExercise(\n geometry: GeometryState,\n exercise: GeometryExercise,\n): GeometryState {\n const dropped = new Set<EntityId>()\n for (const [id, definition] of Object.entries(geometry.entities)) {\n const given = entryOf(exercise.entities, id)\n // A mismatched kind is a genuine collision rather than a re-declaration,\n // and it is refused below rather than silently tidied away here.\n if (!given || given.kind !== 'point' || definition.kind !== 'point') continue\n if (!exercise.draggableEntityIds.includes(id)) dropped.add(id)\n }\n const answerEntityIds = geometry.answerEntityIds.filter(\n (id) => !Object.prototype.hasOwnProperty.call(exercise.entities, id),\n )\n if (dropped.size === 0 && answerEntityIds.length === geometry.answerEntityIds.length) {\n return geometry\n }\n\n const keep = <T>(record: Record<EntityId, T>): Record<EntityId, T> =>\n Object.fromEntries(Object.entries(record).filter(([id]) => !dropped.has(id)))\n return {\n ...geometry,\n entities: keep(geometry.entities),\n presentation: keep(geometry.presentation),\n answerEntityIds,\n }\n}\n\nexport type GeometryStateInputValidationResult =\n | { success: true; data: GeometryState; errors: [] }\n | { success: false; data: null; errors: GeometryExerciseValidationIssue[] }\n\nexport function validateGeometryStateInput(\n input: unknown,\n exercise?: GeometryExercise,\n): GeometryStateInputValidationResult {\n // Stored attempts are brought up to the current version before their\n // structure is checked, so an older saved attempt loads rather than failing\n // as malformed.\n const migrated = migrateGeometryState(input)\n if (!migrated.success) {\n return {\n success: false,\n data: null,\n errors: [{ path: ['schemaVersion'], message: migrated.message }],\n }\n }\n\n const result = geometryStateSchema.safeParse(migrated.data)\n if (!result.success) {\n return {\n success: false,\n data: null,\n errors: result.error.issues.map((issue) => ({\n path: issue.path.map((part) => (typeof part === 'symbol' ? part.toString() : part)),\n message: issue.message,\n })),\n }\n }\n\n const errors: GeometryExerciseValidationIssue[] = []\n let data = result.data\n if (exercise) {\n data = normalizedAgainstExercise(data, exercise)\n\n // An id both documents declare, unless both declare it as a point.\n // `buildGeometryContext()` reports these as `duplicate-id` and then\n // resolves the id to the exercise's declaration, so before M2.G.2 an\n // attempt declaring segment `A` over given point `A` loaded clean and had\n // the *given* graded as the student's construction. One rule, shared with\n // the context, and the document is refused rather than reinterpreted.\n for (const issue of crossDocumentIdCollisionIssues(data, exercise.entities)) {\n errors.push(issue)\n }\n\n // The parents the attempt schema deliberately let through: since the\n // amendment a student's point sits on a given segment, so most of an\n // attempt's parents are declared in the other document. This is where the\n // pair is completed, in the schema's own words.\n for (const issue of crossDocumentParentIssues(data, exercise.entities)) {\n errors.push(issue)\n }\n\n // What the student was allowed to draw. Points are not checked: placing one\n // is how every construction starts, and v1 never asked either.\n for (const [id, definition] of Object.entries(data.entities)) {\n if (definition.kind === 'point') continue\n if (!exercise.allowedObjects.includes(definition.kind)) {\n errors.push({\n path: ['entities', id],\n message: `${definition.kind} is not allowed by this exercise`,\n })\n }\n }\n }\n\n return errors.length > 0\n ? { success: false, data: null, errors }\n : { success: true, data, errors: [] }\n}\n\n/**\n * Restores an attempt from stored JSON — the counterpart to\n * validateGeometryExerciseJson(). Pass the exercise to also check the attempt\n * against it — only allowed object types, parents naming entities that exist in\n * one of the two documents — and to normalize it, which is what drops the given\n * points a migrated v1 attempt still carries.\n */\nexport function validateGeometryStateJson(\n json: string,\n exercise?: GeometryExercise,\n): GeometryStateInputValidationResult {\n try {\n return validateGeometryStateInput(JSON.parse(json), exercise)\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 }\n }\n}\n\n// Saves an attempt. Trivial today, but it is the one place that guarantees the\n// stored document carries its version, so a restore never has to guess.\nexport function serializeGeometryState(geometry: GeometryState): string {\n return JSON.stringify(geometry)\n}\n","import type { GeometryState } from '../schemas/geometryState.schema'\nimport { parentsOfDefinition } from '../geometry/entities'\nimport { entryOf } from '../geometry/entityRecord'\nimport type { GoalStatus, GeometryValidationFeedback } from './types'\n\nexport interface GeometryCanvasFeedback {\n constructions: Record<string, GoalStatus>\n points: Record<string, GoalStatus>\n}\n\nexport function deriveGeometryCanvasFeedback(\n feedback: GeometryValidationFeedback | null,\n geometry: GeometryState,\n focusedGoalId: string | null,\n): GeometryCanvasFeedback {\n const focused = feedback?.goals.find((goal) => goal.id === focusedGoalId)\n if (!focused) return { constructions: {}, points: {} }\n\n const constructions = Object.fromEntries(\n focused.constructionIds.map((id) => [id, focused.status]),\n ) as Record<string, GoalStatus>\n const pointIds = new Set(focused.pointIds)\n for (const [id, definition] of Object.entries(geometry.entities)) {\n if (definition.kind === 'point') continue\n if (!entryOf(constructions, id)) continue\n for (const pointId of parentsOfDefinition(definition)) pointIds.add(pointId)\n }\n const points = Object.fromEntries([...pointIds].map((id) => [id, focused.status])) as Record<\n string,\n GoalStatus\n >\n return { constructions, points }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { withoutSolution } from '../schemas/exerciseProjection'\nimport type { GeometryState } from '../schemas/geometryState.schema'\n\n// Feedback freshness (RFC 0005 § Decisions 2 and 5, ADR 0014). A check result\n// describes the normalized documents it graded, not the objects a host passed\n// in, so a clone, a JSON round trip or a reordered record fingerprints the same,\n// while any change to what grading reads does not.\n//\n// The value is opaque: only another fingerprint from this engine is ever\n// compared against it. That keeps the format free to change — a result from an\n// older format simply stops matching, which hides feedback instead of showing\n// stale feedback.\n\n// JSON with object keys sorted at every depth. Array order is kept because it\n// is meaningful (answer order is report order); records are the documents'\n// only unordered collections. An `undefined` member is skipped as\n// JSON.stringify skips it, so an absent optional field and an explicit\n// `undefined` agree.\nfunction canonicalJson(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`\n }\n if (typeof value === 'object' && value !== null) {\n const record = value as Record<string, unknown>\n const members = Object.keys(record)\n .sort()\n .filter((key) => record[key] !== undefined)\n .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)\n return `{${members.join(',')}}`\n }\n return JSON.stringify(value)\n}\n\n// cyrb53's two 32-bit lanes, both printed: 16 hex digits. Not cryptographic —\n// a fingerprint detects that grading inputs changed; a host forging a match\n// could only ever redisplay feedback it already holds.\nfunction hash(text: string): string {\n let h1 = 0xdeadbeef\n let h2 = 0x41c6ce57\n for (let index = 0; index < text.length; index++) {\n const code = text.charCodeAt(index)\n h1 = Math.imul(h1 ^ code, 2654435761)\n h2 = Math.imul(h2 ^ code, 1597334677)\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)\n return `${(h2 >>> 0).toString(16).padStart(8, '0')}${(h1 >>> 0).toString(16).padStart(8, '0')}`\n}\n\n// The solution-free grading projection. Checking ignores the authored solution\n// ADR 0015 embeds, so changing only that must not hide feedback.\nexport function fingerprintGeometryExercise(exercise: GeometryExercise): string {\n return hash(canonicalJson(withoutSolution(exercise)))\n}\n\n// Takes the attempt as grading read it: migrated, parsed and normalized against\n// its exercise, so a dropped pinned given or an older version changes nothing.\nexport function fingerprintGeometryAttempt(attempt: GeometryState): string {\n return hash(canonicalJson(attempt))\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\n\nexport interface GeometryExerciseWarning {\n path: Array<string | number>\n message: string\n}\n\n/**\n * What is wrong with an exercise without being invalid. Today that is one rule:\n * a given placed where the viewport cannot show it.\n *\n * Only a **free** point is checked. A derived point has no stored coordinates —\n * it is wherever the graph puts it, which depends on the entities it is built\n * on — so warning about it would mean resolving the whole exercise, and where\n * an authoring warning belongs for a construction that lands off-screen is an\n * M3 question about the authoring UI rather than about the document.\n */\nexport function getGeometryExerciseWarnings(exercise: GeometryExercise): GeometryExerciseWarning[] {\n const [xMin, yMin, xMax, yMax] = exercise.viewport\n const warnings: GeometryExerciseWarning[] = []\n for (const [id, definition] of Object.entries(exercise.entities)) {\n if (definition.kind !== 'point' || !('at' in definition)) continue\n const { x, y } = definition.at\n if (x >= xMin && x <= xMax && y >= yMin && y <= yMax) continue\n warnings.push({ path: ['entities', id], message: `point ${id} is outside the viewport` })\n }\n return warnings\n}\n","// The one exercise load every public loader is built on (M3.I.3, option C).\n// Internal: not exported from any entry point.\n//\n// Exercise v4 embeds an authored solution (ADR 0015), and the engine loads an\n// exercise two ways. **Grading** — Check, fingerprints, the student component —\n// must ignore the solution entirely, so a malformed one never disables them.\n// **The full load** — `validateGeometryExercise()` and reveal — must also\n// migrate the solution and check it against the exercise. Both start from\n// `loadExerciseDocument()`, so the exercise half cannot differ between them, and the\n// full load adds `validateEmbeddedSolution()` on top.\nimport { withoutSolution } from '../schemas/exerciseProjection'\nimport {\n geometryExerciseSchema,\n migrateGeometryExercise,\n type GeometryExercise,\n} from '../schemas/exercise.schema'\nimport {\n getGeometryExerciseWarnings,\n type GeometryExerciseWarning,\n} from './getGeometryExerciseWarnings'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\nimport {\n validateGeometryStateInput,\n type GeometryStateInputValidationResult,\n} from './validateGeometryStateInput'\n\nexport type ExerciseLoadResult =\n | {\n success: true\n /** The solution-free projection, parsed. */\n data: GeometryExercise\n /** The stored solution, untouched: `undefined` when the document has none. */\n solution: unknown\n warnings: GeometryExerciseWarning[]\n }\n | { success: false; errors: GeometryExerciseValidationIssue[] }\n\nfunction normalizePath(path: PropertyKey[]): Array<string | number> {\n return path.map((segment) =>\n typeof segment === 'symbol' ? (segment.description ?? segment.toString()) : segment,\n )\n}\n\n/**\n * Migrates a stored exercise and parses its solution-free projection. Every\n * refusal the migration stage can produce — an older version with no step, a\n * newer one, something that is not a document at all — is reported under\n * `schemaVersion`, because that is the field the reader consulted.\n */\nexport function loadExerciseDocument(input: unknown): ExerciseLoadResult {\n const migrated = migrateGeometryExercise(input)\n if (!migrated.success) {\n return { success: false, errors: [{ path: ['schemaVersion'], message: migrated.message }] }\n }\n\n const document = migrated.data as Record<string, unknown>\n const result = geometryExerciseSchema.safeParse(withoutSolution(document))\n if (!result.success) {\n return {\n success: false,\n errors: result.error.issues.map((issue) => ({\n path: normalizePath(issue.path),\n message: issue.message,\n })),\n }\n }\n return {\n success: true,\n data: result.data,\n solution: document.solution,\n warnings: getGeometryExerciseWarnings(result.data),\n }\n}\n\n/**\n * The stored solution as an attempt of `exercise`: migrated on the attempt\n * chain, parsed, and normalized and cross-checked against the exercise exactly\n * as a student attempt is. Issue paths are rooted at `solution`, so they point\n * into the exercise document a host holds.\n */\nexport function validateEmbeddedSolution(\n solution: unknown,\n exercise: GeometryExercise,\n): GeometryStateInputValidationResult {\n const attempt = validateGeometryStateInput(solution, exercise)\n if (attempt.success) return attempt\n return {\n ...attempt,\n errors: attempt.errors.map((issue) => ({ ...issue, path: ['solution', ...issue.path] })),\n }\n}\n","import { latestGeometryStateVersion, type GeometryState } from '../schemas/geometryState.schema'\nimport { validateGeometryStateInput } from './validateGeometryStateInput'\n\n/**\n * A fresh attempt at the current attempt version, for a host starting a student\n * on an exercise (RFC 0005 § Decision 5). It takes no exercise: the two\n * documents version independently, and an empty attempt holds nothing of the\n * exercise. Every call returns a new object, so a host may keep or change one\n * without touching another.\n */\nexport function createEmptyGeometryAttempt(): GeometryState {\n return {\n schemaVersion: latestGeometryStateVersion,\n entities: {},\n presentation: {},\n answerEntityIds: [],\n }\n}\n\n/**\n * Whether a stored attempt holds any student work — a completion hint for a\n * host's \"not started\" state, never a grade. Untrusted input is migrated and\n * parsed first; absent or malformed input is `false` rather than an error.\n *\n * Work is any entity the attempt declares, not only an answer: dragging a given\n * copies it into `entities` while `answerEntityIds` stays empty, and that\n * counts. Reset, or deleting everything the student added, empties `entities`\n * again.\n *\n * No exercise is read, so a pinned given that a migrated v1 attempt still\n * re-declares counts here, although the load pass against its exercise drops it.\n */\nexport function hasGeometryWork(attemptInput: unknown): boolean {\n const attempt = validateGeometryStateInput(attemptInput)\n return attempt.success && Object.keys(attempt.data.entities).length > 0\n}\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- const e=require("./geometryState.schema-DKQ-durx.cjs"),t=require("./exercise.schema-BV2b7rtN.cjs");function n(t,n){let r=new Set;for(let[i,a]of Object.entries(t.entities)){let t=e.O(n.entities,i);!t||t.kind!==`point`||a.kind!==`point`||n.draggableEntityIds.includes(i)||r.add(i)}let i=t.answerEntityIds.filter(e=>!Object.prototype.hasOwnProperty.call(n.entities,e));if(r.size===0&&i.length===t.answerEntityIds.length)return t;let a=e=>Object.fromEntries(Object.entries(e).filter(([e])=>!r.has(e)));return{...t,entities:a(t.entities),presentation:a(t.presentation),answerEntityIds:i}}function r(t,r){let i=e.i(t);if(!i.success)return{success:!1,data:null,errors:[{path:[`schemaVersion`],message:i.message}]};let a=e.n.safeParse(i.data);if(!a.success)return{success:!1,data:null,errors:a.error.issues.map(e=>({path:e.path.map(e=>typeof e==`symbol`?e.toString():e),message:e.message}))};let o=[],s=a.data;if(r){s=n(s,r);for(let t of e.x(s,r.entities))o.push(t);for(let t of e.S(s,r.entities))o.push(t);for(let[e,t]of Object.entries(s.entities))t.kind!==`point`&&(r.allowedObjects.includes(t.kind)||o.push({path:[`entities`,e],message:`${t.kind} is not allowed by this exercise`}))}return o.length>0?{success:!1,data:null,errors:o}:{success:!0,data:s,errors:[]}}function i(e,t){try{return r(JSON.parse(e),t)}catch(e){return{success:!1,data:null,errors:[{path:[],message:e instanceof Error?e.message:`Invalid JSON syntax`}]}}}function a(e){return JSON.stringify(e)}function o(t,n,r){let i=t?.goals.find(e=>e.id===r);if(!i)return{constructions:{},points:{}};let a=Object.fromEntries(i.constructionIds.map(e=>[e,i.status])),o=new Set(i.pointIds);for(let[t,r]of Object.entries(n.entities))if(r.kind!==`point`&&e.O(a,t))for(let t of e.F(r))o.add(t);return{constructions:a,points:Object.fromEntries([...o].map(e=>[e,i.status]))}}function s(e){if(Array.isArray(e))return`[${e.map(e=>e===void 0?`null`:s(e)).join(`,`)}]`;if(typeof e==`object`&&e){let t=e;return`{${Object.keys(t).sort().filter(e=>t[e]!==void 0).map(e=>`${JSON.stringify(e)}:${s(t[e])}`).join(`,`)}}`}return JSON.stringify(e)}function c(e){let t=3735928559,n=1103547991;for(let r=0;r<e.length;r++){let i=e.charCodeAt(r);t=Math.imul(t^i,2654435761),n=Math.imul(n^i,1597334677)}return t=Math.imul(t^t>>>16,2246822507),t^=Math.imul(n^n>>>13,3266489909),n=Math.imul(n^n>>>16,2246822507),n^=Math.imul(t^t>>>13,3266489909),`${(n>>>0).toString(16).padStart(8,`0`)}${(t>>>0).toString(16).padStart(8,`0`)}`}function l(t){return c(s(e.u(t)))}function u(e){return c(s(e))}function d(e){let[t,n,r,i]=e.viewport,a=[];for(let[o,s]of Object.entries(e.entities)){if(s.kind!==`point`||!(`at`in s))continue;let{x:e,y:c}=s.at;e>=t&&e<=r&&c>=n&&c<=i||a.push({path:[`entities`,o],message:`point ${o} is outside the viewport`})}return a}function f(e){return e.map(e=>typeof e==`symbol`?e.description??e.toString():e)}function p(n){let r=t.o(n);if(!r.success)return{success:!1,errors:[{path:[`schemaVersion`],message:r.message}]};let i=r.data,a=t.i.safeParse(e.u(i));return a.success?{success:!0,data:a.data,solution:i.solution,warnings:d(a.data)}:{success:!1,errors:a.error.issues.map(e=>({path:f(e.path),message:e.message}))}}function m(e,t){let n=r(e,t);return n.success?n:{...n,errors:n.errors.map(e=>({...e,path:[`solution`,...e.path]}))}}function h(){return{schemaVersion:2,entities:{},presentation:{},answerEntityIds:[]}}function g(e){let t=r(e);return t.success&&Object.keys(t.data.entities).length>0}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return r}});
2
- //# sourceMappingURL=geometryAttemptStatus-N_XHIkiS.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"geometryAttemptStatus-N_XHIkiS.cjs","names":[],"sources":["../src/validation/validateGeometryStateInput.ts","../src/validation/deriveGeometryCanvasFeedback.ts","../src/validation/gradingFingerprints.ts","../src/validation/getGeometryExerciseWarnings.ts","../src/validation/exerciseLoad.ts","../src/validation/geometryAttemptStatus.ts"],"sourcesContent":["import type { EntityId } from '../geometry/entityModel'\nimport { entryOf } from '../geometry/entityRecord'\nimport {\n crossDocumentIdCollisionIssues,\n crossDocumentParentIssues,\n} from '../schemas/entityDocument'\nimport type { GeometryExercise } from '../schemas/exercise.schema'\nimport {\n geometryStateSchema,\n migrateGeometryState,\n type GeometryState,\n} from '../schemas/geometryState.schema'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\n\n/**\n * The load pass ADR 0008 § Amendment specifies: a v2 attempt carries **only\n * student work**, so an entity re-declaring a *pinned* given point is removed\n * on the way in, and no given at all survives in `answerEntityIds`.\n *\n * Neither drop is an error, because both are what every migrated v1 attempt\n * looks like — v1 seeded the givens into the attempt, required them to be\n * there, and its reveal promoted a given point into `constructedPointIds`,\n * which the migration copies verbatim into `answerEntityIds`. A *draggable*\n * given entity is kept: re-declaring one is how the student moves it.\n *\n * A dropped entity's `presentation` entry goes with it, both because a\n * leftover would make the normalized document fail the schema it just passed,\n * and because it would be saying something about an entity this document no\n * longer declares.\n *\n * **`answerEntityIds` loses every id the exercise declares**, not only the\n * dropped ones: a given is not an answer to anything, so a draggable given the\n * student moved is still not something they built. Dropping rather than\n * refusing is decided by what the loader can see — a hand-written document\n * naming a given there is indistinguishable from a migrated v1 one, and\n * refusing would make every stored v1 attempt with a revealed given\n * unloadable. The entry says nothing true either way, so it is removed in\n * silence, exactly as the pinned re-declaration is.\n */\nfunction normalizedAgainstExercise(\n geometry: GeometryState,\n exercise: GeometryExercise,\n): GeometryState {\n const dropped = new Set<EntityId>()\n for (const [id, definition] of Object.entries(geometry.entities)) {\n const given = entryOf(exercise.entities, id)\n // A mismatched kind is a genuine collision rather than a re-declaration,\n // and it is refused below rather than silently tidied away here.\n if (!given || given.kind !== 'point' || definition.kind !== 'point') continue\n if (!exercise.draggableEntityIds.includes(id)) dropped.add(id)\n }\n const answerEntityIds = geometry.answerEntityIds.filter(\n (id) => !Object.prototype.hasOwnProperty.call(exercise.entities, id),\n )\n if (dropped.size === 0 && answerEntityIds.length === geometry.answerEntityIds.length) {\n return geometry\n }\n\n const keep = <T>(record: Record<EntityId, T>): Record<EntityId, T> =>\n Object.fromEntries(Object.entries(record).filter(([id]) => !dropped.has(id)))\n return {\n ...geometry,\n entities: keep(geometry.entities),\n presentation: keep(geometry.presentation),\n answerEntityIds,\n }\n}\n\nexport type GeometryStateInputValidationResult =\n | { success: true; data: GeometryState; errors: [] }\n | { success: false; data: null; errors: GeometryExerciseValidationIssue[] }\n\nexport function validateGeometryStateInput(\n input: unknown,\n exercise?: GeometryExercise,\n): GeometryStateInputValidationResult {\n // Stored attempts are brought up to the current version before their\n // structure is checked, so an older saved attempt loads rather than failing\n // as malformed.\n const migrated = migrateGeometryState(input)\n if (!migrated.success) {\n return {\n success: false,\n data: null,\n errors: [{ path: ['schemaVersion'], message: migrated.message }],\n }\n }\n\n const result = geometryStateSchema.safeParse(migrated.data)\n if (!result.success) {\n return {\n success: false,\n data: null,\n errors: result.error.issues.map((issue) => ({\n path: issue.path.map((part) => (typeof part === 'symbol' ? part.toString() : part)),\n message: issue.message,\n })),\n }\n }\n\n const errors: GeometryExerciseValidationIssue[] = []\n let data = result.data\n if (exercise) {\n data = normalizedAgainstExercise(data, exercise)\n\n // An id both documents declare, unless both declare it as a point.\n // `buildGeometryContext()` reports these as `duplicate-id` and then\n // resolves the id to the exercise's declaration, so before M2.G.2 an\n // attempt declaring segment `A` over given point `A` loaded clean and had\n // the *given* graded as the student's construction. One rule, shared with\n // the context, and the document is refused rather than reinterpreted.\n for (const issue of crossDocumentIdCollisionIssues(data, exercise.entities)) {\n errors.push(issue)\n }\n\n // The parents the attempt schema deliberately let through: since the\n // amendment a student's point sits on a given segment, so most of an\n // attempt's parents are declared in the other document. This is where the\n // pair is completed, in the schema's own words.\n for (const issue of crossDocumentParentIssues(data, exercise.entities)) {\n errors.push(issue)\n }\n\n // What the student was allowed to draw. Points are not checked: placing one\n // is how every construction starts, and v1 never asked either.\n for (const [id, definition] of Object.entries(data.entities)) {\n if (definition.kind === 'point') continue\n if (!exercise.allowedObjects.includes(definition.kind)) {\n errors.push({\n path: ['entities', id],\n message: `${definition.kind} is not allowed by this exercise`,\n })\n }\n }\n }\n\n return errors.length > 0\n ? { success: false, data: null, errors }\n : { success: true, data, errors: [] }\n}\n\n/**\n * Restores an attempt from stored JSON — the counterpart to\n * validateGeometryExerciseJson(). Pass the exercise to also check the attempt\n * against it — only allowed object types, parents naming entities that exist in\n * one of the two documents — and to normalize it, which is what drops the given\n * points a migrated v1 attempt still carries.\n */\nexport function validateGeometryStateJson(\n json: string,\n exercise?: GeometryExercise,\n): GeometryStateInputValidationResult {\n try {\n return validateGeometryStateInput(JSON.parse(json), exercise)\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 }\n }\n}\n\n// Saves an attempt. Trivial today, but it is the one place that guarantees the\n// stored document carries its version, so a restore never has to guess.\nexport function serializeGeometryState(geometry: GeometryState): string {\n return JSON.stringify(geometry)\n}\n","import type { GeometryState } from '../schemas/geometryState.schema'\nimport { parentsOfDefinition } from '../geometry/entities'\nimport { entryOf } from '../geometry/entityRecord'\nimport type { GoalStatus, GeometryValidationFeedback } from './types'\n\nexport interface GeometryCanvasFeedback {\n constructions: Record<string, GoalStatus>\n points: Record<string, GoalStatus>\n}\n\nexport function deriveGeometryCanvasFeedback(\n feedback: GeometryValidationFeedback | null,\n geometry: GeometryState,\n focusedGoalId: string | null,\n): GeometryCanvasFeedback {\n const focused = feedback?.goals.find((goal) => goal.id === focusedGoalId)\n if (!focused) return { constructions: {}, points: {} }\n\n const constructions = Object.fromEntries(\n focused.constructionIds.map((id) => [id, focused.status]),\n ) as Record<string, GoalStatus>\n const pointIds = new Set(focused.pointIds)\n for (const [id, definition] of Object.entries(geometry.entities)) {\n if (definition.kind === 'point') continue\n if (!entryOf(constructions, id)) continue\n for (const pointId of parentsOfDefinition(definition)) pointIds.add(pointId)\n }\n const points = Object.fromEntries([...pointIds].map((id) => [id, focused.status])) as Record<\n string,\n GoalStatus\n >\n return { constructions, points }\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\nimport { withoutSolution } from '../schemas/exerciseProjection'\nimport type { GeometryState } from '../schemas/geometryState.schema'\n\n// Feedback freshness (RFC 0005 § Decisions 2 and 5, ADR 0014). A check result\n// describes the normalized documents it graded, not the objects a host passed\n// in, so a clone, a JSON round trip or a reordered record fingerprints the same,\n// while any change to what grading reads does not.\n//\n// The value is opaque: only another fingerprint from this engine is ever\n// compared against it. That keeps the format free to change — a result from an\n// older format simply stops matching, which hides feedback instead of showing\n// stale feedback.\n\n// JSON with object keys sorted at every depth. Array order is kept because it\n// is meaningful (answer order is report order); records are the documents'\n// only unordered collections. An `undefined` member is skipped as\n// JSON.stringify skips it, so an absent optional field and an explicit\n// `undefined` agree.\nfunction canonicalJson(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map((item) => (item === undefined ? 'null' : canonicalJson(item))).join(',')}]`\n }\n if (typeof value === 'object' && value !== null) {\n const record = value as Record<string, unknown>\n const members = Object.keys(record)\n .sort()\n .filter((key) => record[key] !== undefined)\n .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)\n return `{${members.join(',')}}`\n }\n return JSON.stringify(value)\n}\n\n// cyrb53's two 32-bit lanes, both printed: 16 hex digits. Not cryptographic —\n// a fingerprint detects that grading inputs changed; a host forging a match\n// could only ever redisplay feedback it already holds.\nfunction hash(text: string): string {\n let h1 = 0xdeadbeef\n let h2 = 0x41c6ce57\n for (let index = 0; index < text.length; index++) {\n const code = text.charCodeAt(index)\n h1 = Math.imul(h1 ^ code, 2654435761)\n h2 = Math.imul(h2 ^ code, 1597334677)\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)\n return `${(h2 >>> 0).toString(16).padStart(8, '0')}${(h1 >>> 0).toString(16).padStart(8, '0')}`\n}\n\n// The solution-free grading projection. Checking ignores the authored solution\n// ADR 0015 embeds, so changing only that must not hide feedback.\nexport function fingerprintGeometryExercise(exercise: GeometryExercise): string {\n return hash(canonicalJson(withoutSolution(exercise)))\n}\n\n// Takes the attempt as grading read it: migrated, parsed and normalized against\n// its exercise, so a dropped pinned given or an older version changes nothing.\nexport function fingerprintGeometryAttempt(attempt: GeometryState): string {\n return hash(canonicalJson(attempt))\n}\n","import type { GeometryExercise } from '../schemas/exercise.schema'\n\nexport interface GeometryExerciseWarning {\n path: Array<string | number>\n message: string\n}\n\n/**\n * What is wrong with an exercise without being invalid. Today that is one rule:\n * a given placed where the viewport cannot show it.\n *\n * Only a **free** point is checked. A derived point has no stored coordinates —\n * it is wherever the graph puts it, which depends on the entities it is built\n * on — so warning about it would mean resolving the whole exercise, and where\n * an authoring warning belongs for a construction that lands off-screen is an\n * M3 question about the authoring UI rather than about the document.\n */\nexport function getGeometryExerciseWarnings(exercise: GeometryExercise): GeometryExerciseWarning[] {\n const [xMin, yMin, xMax, yMax] = exercise.viewport\n const warnings: GeometryExerciseWarning[] = []\n for (const [id, definition] of Object.entries(exercise.entities)) {\n if (definition.kind !== 'point' || !('at' in definition)) continue\n const { x, y } = definition.at\n if (x >= xMin && x <= xMax && y >= yMin && y <= yMax) continue\n warnings.push({ path: ['entities', id], message: `point ${id} is outside the viewport` })\n }\n return warnings\n}\n","// The one exercise load every public loader is built on (M3.I.3, option C).\n// Internal: not exported from any entry point.\n//\n// Exercise v4 embeds an authored solution (ADR 0015), and the engine loads an\n// exercise two ways. **Grading** — Check, fingerprints, the student component —\n// must ignore the solution entirely, so a malformed one never disables them.\n// **The full load** — `validateGeometryExercise()` and reveal — must also\n// migrate the solution and check it against the exercise. Both start from\n// `loadExerciseDocument()`, so the exercise half cannot differ between them, and the\n// full load adds `validateEmbeddedSolution()` on top.\nimport { withoutSolution } from '../schemas/exerciseProjection'\nimport {\n geometryExerciseSchema,\n migrateGeometryExercise,\n type GeometryExercise,\n} from '../schemas/exercise.schema'\nimport {\n getGeometryExerciseWarnings,\n type GeometryExerciseWarning,\n} from './getGeometryExerciseWarnings'\nimport type { GeometryExerciseValidationIssue } from './validateGeometryExercise'\nimport {\n validateGeometryStateInput,\n type GeometryStateInputValidationResult,\n} from './validateGeometryStateInput'\n\nexport type ExerciseLoadResult =\n | {\n success: true\n /** The solution-free projection, parsed. */\n data: GeometryExercise\n /** The stored solution, untouched: `undefined` when the document has none. */\n solution: unknown\n warnings: GeometryExerciseWarning[]\n }\n | { success: false; errors: GeometryExerciseValidationIssue[] }\n\nfunction normalizePath(path: PropertyKey[]): Array<string | number> {\n return path.map((segment) =>\n typeof segment === 'symbol' ? (segment.description ?? segment.toString()) : segment,\n )\n}\n\n/**\n * Migrates a stored exercise and parses its solution-free projection. Every\n * refusal the migration stage can produce — an older version with no step, a\n * newer one, something that is not a document at all — is reported under\n * `schemaVersion`, because that is the field the reader consulted.\n */\nexport function loadExerciseDocument(input: unknown): ExerciseLoadResult {\n const migrated = migrateGeometryExercise(input)\n if (!migrated.success) {\n return { success: false, errors: [{ path: ['schemaVersion'], message: migrated.message }] }\n }\n\n const document = migrated.data as Record<string, unknown>\n const result = geometryExerciseSchema.safeParse(withoutSolution(document))\n if (!result.success) {\n return {\n success: false,\n errors: result.error.issues.map((issue) => ({\n path: normalizePath(issue.path),\n message: issue.message,\n })),\n }\n }\n return {\n success: true,\n data: result.data,\n solution: document.solution,\n warnings: getGeometryExerciseWarnings(result.data),\n }\n}\n\n/**\n * The stored solution as an attempt of `exercise`: migrated on the attempt\n * chain, parsed, and normalized and cross-checked against the exercise exactly\n * as a student attempt is. Issue paths are rooted at `solution`, so they point\n * into the exercise document a host holds.\n */\nexport function validateEmbeddedSolution(\n solution: unknown,\n exercise: GeometryExercise,\n): GeometryStateInputValidationResult {\n const attempt = validateGeometryStateInput(solution, exercise)\n if (attempt.success) return attempt\n return {\n ...attempt,\n errors: attempt.errors.map((issue) => ({ ...issue, path: ['solution', ...issue.path] })),\n }\n}\n","import { latestGeometryStateVersion, type GeometryState } from '../schemas/geometryState.schema'\nimport { validateGeometryStateInput } from './validateGeometryStateInput'\n\n/**\n * A fresh attempt at the current attempt version, for a host starting a student\n * on an exercise (RFC 0005 § Decision 5). It takes no exercise: the two\n * documents version independently, and an empty attempt holds nothing of the\n * exercise. Every call returns a new object, so a host may keep or change one\n * without touching another.\n */\nexport function createEmptyGeometryAttempt(): GeometryState {\n return {\n schemaVersion: latestGeometryStateVersion,\n entities: {},\n presentation: {},\n answerEntityIds: [],\n }\n}\n\n/**\n * Whether a stored attempt holds any student work — a completion hint for a\n * host's \"not started\" state, never a grade. Untrusted input is migrated and\n * parsed first; absent or malformed input is `false` rather than an error.\n *\n * Work is any entity the attempt declares, not only an answer: dragging a given\n * copies it into `entities` while `answerEntityIds` stays empty, and that\n * counts. Reset, or deleting everything the student added, empties `entities`\n * again.\n *\n * No exercise is read, so a pinned given that a migrated v1 attempt still\n * re-declares counts here, although the load pass against its exercise drops it.\n */\nexport function hasGeometryWork(attemptInput: unknown): boolean {\n const attempt = validateGeometryStateInput(attemptInput)\n return attempt.success && Object.keys(attempt.data.entities).length > 0\n}\n"],"mappings":"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"}