@vived/app 6.6.0 → 6.7.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 (176) hide show
  1. package/dist/cjs/BaseApp/UC/RenderCanvasUC.js +17 -36
  2. package/dist/cjs/BaseApp/UC/RenderCanvasUC.js.map +1 -1
  3. package/dist/cjs/BaseApp/UC/RenderCanvasUC.test.js +18 -18
  4. package/dist/cjs/BaseApp/UC/RenderCanvasUC.test.js.map +1 -1
  5. package/dist/cjs/Challenge/Factories/ChallengeFeatureFactory.js +9 -28
  6. package/dist/cjs/Challenge/Factories/ChallengeFeatureFactory.js.map +1 -1
  7. package/dist/cjs/Challenge/Factories/ChallengeFeatureFactory.test.js +51 -0
  8. package/dist/cjs/Challenge/Factories/ChallengeFeatureFactory.test.js.map +1 -0
  9. package/dist/cjs/EventSystem/Entities/EventEntity.js +12 -2
  10. package/dist/cjs/EventSystem/Entities/EventEntity.js.map +1 -1
  11. package/dist/cjs/EventSystem/Entities/EventEntity.test.js +45 -0
  12. package/dist/cjs/EventSystem/Entities/EventEntity.test.js.map +1 -1
  13. package/dist/cjs/EventSystem/Mocks/MockEventEntity.js +7 -2
  14. package/dist/cjs/EventSystem/Mocks/MockEventEntity.js.map +1 -1
  15. package/dist/cjs/EventSystem/types.js.map +1 -1
  16. package/dist/cjs/ObjectPick/Factories/setupObjectPick.js +18 -1
  17. package/dist/cjs/ObjectPick/Factories/setupObjectPick.js.map +1 -1
  18. package/dist/cjs/ObjectPick/Factories/setupObjectPick.test.js +55 -0
  19. package/dist/cjs/ObjectPick/Factories/setupObjectPick.test.js.map +1 -0
  20. package/dist/cjs/Procedure/Adapters/procedureAdapter.js +1 -0
  21. package/dist/cjs/Procedure/Adapters/procedureAdapter.js.map +1 -1
  22. package/dist/cjs/Procedure/Adapters/procedureAdapter.test.js +1 -0
  23. package/dist/cjs/Procedure/Adapters/procedureAdapter.test.js.map +1 -1
  24. package/dist/cjs/Procedure/Adapters/procedureAuthoringAdapter.js +2 -0
  25. package/dist/cjs/Procedure/Adapters/procedureAuthoringAdapter.js.map +1 -1
  26. package/dist/cjs/Procedure/Adapters/procedureAuthoringAdapter.test.js +3 -0
  27. package/dist/cjs/Procedure/Adapters/procedureAuthoringAdapter.test.js.map +1 -1
  28. package/dist/cjs/Procedure/Adapters/procedureHintAdapter.test.js +1 -0
  29. package/dist/cjs/Procedure/Adapters/procedureHintAdapter.test.js.map +1 -1
  30. package/dist/cjs/Procedure/Adapters/procedureProgressAdapter.js +2 -0
  31. package/dist/cjs/Procedure/Adapters/procedureProgressAdapter.js.map +1 -1
  32. package/dist/cjs/Procedure/Adapters/procedureProgressAdapter.test.js +3 -0
  33. package/dist/cjs/Procedure/Adapters/procedureProgressAdapter.test.js.map +1 -1
  34. package/dist/cjs/Procedure/Adapters/procedureQuizResultAdapter.js +2 -0
  35. package/dist/cjs/Procedure/Adapters/procedureQuizResultAdapter.js.map +1 -1
  36. package/dist/cjs/Procedure/Adapters/procedureQuizResultAdapter.test.js +3 -0
  37. package/dist/cjs/Procedure/Adapters/procedureQuizResultAdapter.test.js.map +1 -1
  38. package/dist/cjs/Procedure/Entities/ProcedureEntity.js +163 -24
  39. package/dist/cjs/Procedure/Entities/ProcedureEntity.js.map +1 -1
  40. package/dist/cjs/Procedure/Entities/ProcedureEntity.test.js +240 -0
  41. package/dist/cjs/Procedure/Entities/ProcedureEntity.test.js.map +1 -1
  42. package/dist/cjs/Procedure/Factories/ProcedureFeatureFactory.js +20 -0
  43. package/dist/cjs/Procedure/Factories/ProcedureFeatureFactory.js.map +1 -1
  44. package/dist/cjs/Procedure/Factories/ProcedureFeatureFactory.test.js +25 -0
  45. package/dist/cjs/Procedure/Factories/ProcedureFeatureFactory.test.js.map +1 -1
  46. package/dist/cjs/Procedure/Mocks/MockProcedureEntity.js +2 -0
  47. package/dist/cjs/Procedure/Mocks/MockProcedureEntity.js.map +1 -1
  48. package/dist/cjs/Procedure/PM/ProcedurePM.js +13 -1
  49. package/dist/cjs/Procedure/PM/ProcedurePM.js.map +1 -1
  50. package/dist/cjs/Procedure/PM/ProcedurePM.test.js +34 -0
  51. package/dist/cjs/Procedure/PM/ProcedurePM.test.js.map +1 -1
  52. package/dist/cjs/Procedure/UCs/UpdateProcedureEventsUCHook.test.js +3 -0
  53. package/dist/cjs/Procedure/UCs/UpdateProcedureEventsUCHook.test.js.map +1 -1
  54. package/dist/cjs/Procedure/types.js.map +1 -1
  55. package/dist/cjs/TouchController/UC/UpdateTouchInoutBound.test.js +37 -0
  56. package/dist/cjs/TouchController/UC/UpdateTouchInoutBound.test.js.map +1 -1
  57. package/dist/cjs/TouchController/UC/UpdateTouchInputBound.js +17 -4
  58. package/dist/cjs/TouchController/UC/UpdateTouchInputBound.js.map +1 -1
  59. package/dist/esm/BaseApp/UC/RenderCanvasUC.js +17 -36
  60. package/dist/esm/BaseApp/UC/RenderCanvasUC.js.map +1 -1
  61. package/dist/esm/BaseApp/UC/RenderCanvasUC.test.js +18 -18
  62. package/dist/esm/BaseApp/UC/RenderCanvasUC.test.js.map +1 -1
  63. package/dist/esm/Challenge/Factories/ChallengeFeatureFactory.js +9 -28
  64. package/dist/esm/Challenge/Factories/ChallengeFeatureFactory.js.map +1 -1
  65. package/dist/esm/Challenge/Factories/ChallengeFeatureFactory.test.js +49 -0
  66. package/dist/esm/Challenge/Factories/ChallengeFeatureFactory.test.js.map +1 -0
  67. package/dist/esm/EventSystem/Entities/EventEntity.js +12 -2
  68. package/dist/esm/EventSystem/Entities/EventEntity.js.map +1 -1
  69. package/dist/esm/EventSystem/Entities/EventEntity.test.js +45 -0
  70. package/dist/esm/EventSystem/Entities/EventEntity.test.js.map +1 -1
  71. package/dist/esm/EventSystem/Mocks/MockEventEntity.js +7 -2
  72. package/dist/esm/EventSystem/Mocks/MockEventEntity.js.map +1 -1
  73. package/dist/esm/EventSystem/types.js.map +1 -1
  74. package/dist/esm/ObjectPick/Factories/setupObjectPick.js +18 -1
  75. package/dist/esm/ObjectPick/Factories/setupObjectPick.js.map +1 -1
  76. package/dist/esm/ObjectPick/Factories/setupObjectPick.test.js +53 -0
  77. package/dist/esm/ObjectPick/Factories/setupObjectPick.test.js.map +1 -0
  78. package/dist/esm/Procedure/Adapters/procedureAdapter.js +1 -0
  79. package/dist/esm/Procedure/Adapters/procedureAdapter.js.map +1 -1
  80. package/dist/esm/Procedure/Adapters/procedureAdapter.test.js +1 -0
  81. package/dist/esm/Procedure/Adapters/procedureAdapter.test.js.map +1 -1
  82. package/dist/esm/Procedure/Adapters/procedureAuthoringAdapter.js +2 -0
  83. package/dist/esm/Procedure/Adapters/procedureAuthoringAdapter.js.map +1 -1
  84. package/dist/esm/Procedure/Adapters/procedureAuthoringAdapter.test.js +3 -0
  85. package/dist/esm/Procedure/Adapters/procedureAuthoringAdapter.test.js.map +1 -1
  86. package/dist/esm/Procedure/Adapters/procedureHintAdapter.test.js +1 -0
  87. package/dist/esm/Procedure/Adapters/procedureHintAdapter.test.js.map +1 -1
  88. package/dist/esm/Procedure/Adapters/procedureProgressAdapter.js +2 -0
  89. package/dist/esm/Procedure/Adapters/procedureProgressAdapter.js.map +1 -1
  90. package/dist/esm/Procedure/Adapters/procedureProgressAdapter.test.js +3 -0
  91. package/dist/esm/Procedure/Adapters/procedureProgressAdapter.test.js.map +1 -1
  92. package/dist/esm/Procedure/Adapters/procedureQuizResultAdapter.js +2 -0
  93. package/dist/esm/Procedure/Adapters/procedureQuizResultAdapter.js.map +1 -1
  94. package/dist/esm/Procedure/Adapters/procedureQuizResultAdapter.test.js +3 -0
  95. package/dist/esm/Procedure/Adapters/procedureQuizResultAdapter.test.js.map +1 -1
  96. package/dist/esm/Procedure/Entities/ProcedureEntity.js +163 -24
  97. package/dist/esm/Procedure/Entities/ProcedureEntity.js.map +1 -1
  98. package/dist/esm/Procedure/Entities/ProcedureEntity.test.js +240 -0
  99. package/dist/esm/Procedure/Entities/ProcedureEntity.test.js.map +1 -1
  100. package/dist/esm/Procedure/Factories/ProcedureFeatureFactory.js +20 -0
  101. package/dist/esm/Procedure/Factories/ProcedureFeatureFactory.js.map +1 -1
  102. package/dist/esm/Procedure/Factories/ProcedureFeatureFactory.test.js +26 -1
  103. package/dist/esm/Procedure/Factories/ProcedureFeatureFactory.test.js.map +1 -1
  104. package/dist/esm/Procedure/Mocks/MockProcedureEntity.js +2 -0
  105. package/dist/esm/Procedure/Mocks/MockProcedureEntity.js.map +1 -1
  106. package/dist/esm/Procedure/PM/ProcedurePM.js +13 -1
  107. package/dist/esm/Procedure/PM/ProcedurePM.js.map +1 -1
  108. package/dist/esm/Procedure/PM/ProcedurePM.test.js +34 -0
  109. package/dist/esm/Procedure/PM/ProcedurePM.test.js.map +1 -1
  110. package/dist/esm/Procedure/UCs/UpdateProcedureEventsUCHook.test.js +3 -0
  111. package/dist/esm/Procedure/UCs/UpdateProcedureEventsUCHook.test.js.map +1 -1
  112. package/dist/esm/Procedure/types.js.map +1 -1
  113. package/dist/esm/TouchController/UC/UpdateTouchInoutBound.test.js +37 -0
  114. package/dist/esm/TouchController/UC/UpdateTouchInoutBound.test.js.map +1 -1
  115. package/dist/esm/TouchController/UC/UpdateTouchInputBound.js +17 -4
  116. package/dist/esm/TouchController/UC/UpdateTouchInputBound.js.map +1 -1
  117. package/dist/types/Challenge/Factories/ChallengeFeatureFactory.d.ts +7 -16
  118. package/dist/types/Challenge/Factories/ChallengeFeatureFactory.d.ts.map +1 -1
  119. package/dist/types/Challenge/Factories/ChallengeFeatureFactory.test.d.ts +2 -0
  120. package/dist/types/Challenge/Factories/ChallengeFeatureFactory.test.d.ts.map +1 -0
  121. package/dist/types/EventSystem/Entities/EventEntity.d.ts +13 -2
  122. package/dist/types/EventSystem/Entities/EventEntity.d.ts.map +1 -1
  123. package/dist/types/EventSystem/Mocks/MockEventEntity.d.ts +3 -2
  124. package/dist/types/EventSystem/Mocks/MockEventEntity.d.ts.map +1 -1
  125. package/dist/types/EventSystem/types.d.ts +23 -0
  126. package/dist/types/EventSystem/types.d.ts.map +1 -1
  127. package/dist/types/ObjectPick/Factories/setupObjectPick.d.ts +10 -0
  128. package/dist/types/ObjectPick/Factories/setupObjectPick.d.ts.map +1 -1
  129. package/dist/types/ObjectPick/Factories/setupObjectPick.test.d.ts +2 -0
  130. package/dist/types/ObjectPick/Factories/setupObjectPick.test.d.ts.map +1 -0
  131. package/dist/types/Procedure/Adapters/procedureAdapter.d.ts.map +1 -1
  132. package/dist/types/Procedure/Adapters/procedureAuthoringAdapter.d.ts.map +1 -1
  133. package/dist/types/Procedure/Adapters/procedureProgressAdapter.d.ts.map +1 -1
  134. package/dist/types/Procedure/Adapters/procedureQuizResultAdapter.d.ts.map +1 -1
  135. package/dist/types/Procedure/Entities/ProcedureEntity.d.ts +10 -0
  136. package/dist/types/Procedure/Entities/ProcedureEntity.d.ts.map +1 -1
  137. package/dist/types/Procedure/Factories/ProcedureFeatureFactory.d.ts +6 -0
  138. package/dist/types/Procedure/Factories/ProcedureFeatureFactory.d.ts.map +1 -1
  139. package/dist/types/Procedure/Mocks/MockProcedureEntity.d.ts +3 -0
  140. package/dist/types/Procedure/Mocks/MockProcedureEntity.d.ts.map +1 -1
  141. package/dist/types/Procedure/types.d.ts +17 -1
  142. package/dist/types/Procedure/types.d.ts.map +1 -1
  143. package/dist/types/TouchController/UC/UpdateTouchInputBound.d.ts +11 -0
  144. package/dist/types/TouchController/UC/UpdateTouchInputBound.d.ts.map +1 -1
  145. package/package.json +6 -3
  146. package/src/EventSystem/CONTEXT.md +29 -17
  147. package/dist/cjs/FramedScreen/Factories/setupStandardFramedScreen.test.js +0 -31
  148. package/dist/cjs/FramedScreen/Factories/setupStandardFramedScreen.test.js.map +0 -1
  149. package/dist/cjs/StateMachine/Controller/clickRestoreScene.js +0 -14
  150. package/dist/cjs/StateMachine/Controller/clickRestoreScene.js.map +0 -1
  151. package/dist/cjs/StateMachine/Controller/clickRestoreScene.test.js +0 -26
  152. package/dist/cjs/StateMachine/Controller/clickRestoreScene.test.js.map +0 -1
  153. package/dist/cjs/TestUtils/index.js +0 -18
  154. package/dist/cjs/TestUtils/index.js.map +0 -1
  155. package/dist/cjs/TestUtils/setupFeatureForTesting.js +0 -60
  156. package/dist/cjs/TestUtils/setupFeatureForTesting.js.map +0 -1
  157. package/dist/esm/FramedScreen/Factories/setupStandardFramedScreen.test.js +0 -29
  158. package/dist/esm/FramedScreen/Factories/setupStandardFramedScreen.test.js.map +0 -1
  159. package/dist/esm/StateMachine/Controller/clickRestoreScene.js +0 -11
  160. package/dist/esm/StateMachine/Controller/clickRestoreScene.js.map +0 -1
  161. package/dist/esm/StateMachine/Controller/clickRestoreScene.test.js +0 -24
  162. package/dist/esm/StateMachine/Controller/clickRestoreScene.test.js.map +0 -1
  163. package/dist/esm/TestUtils/index.js +0 -2
  164. package/dist/esm/TestUtils/index.js.map +0 -1
  165. package/dist/esm/TestUtils/setupFeatureForTesting.js +0 -57
  166. package/dist/esm/TestUtils/setupFeatureForTesting.js.map +0 -1
  167. package/dist/types/FramedScreen/Factories/setupStandardFramedScreen.test.d.ts +0 -2
  168. package/dist/types/FramedScreen/Factories/setupStandardFramedScreen.test.d.ts.map +0 -1
  169. package/dist/types/StateMachine/Controller/clickRestoreScene.d.ts +0 -3
  170. package/dist/types/StateMachine/Controller/clickRestoreScene.d.ts.map +0 -1
  171. package/dist/types/StateMachine/Controller/clickRestoreScene.test.d.ts +0 -2
  172. package/dist/types/StateMachine/Controller/clickRestoreScene.test.d.ts.map +0 -1
  173. package/dist/types/TestUtils/index.d.ts +0 -2
  174. package/dist/types/TestUtils/index.d.ts.map +0 -1
  175. package/dist/types/TestUtils/setupFeatureForTesting.d.ts +0 -48
  176. package/dist/types/TestUtils/setupFeatureForTesting.d.ts.map +0 -1
@@ -18,6 +18,7 @@ describe("procedureQuizResultAdapter", () => {
18
18
  expect(defaultVM.userEventLog).toEqual([]);
19
19
  expect(defaultVM.score).toBeNull();
20
20
  expect(defaultVM.isSubmitted).toBe(false);
21
+ expect(defaultVM.stepCategories).toEqual({});
21
22
  });
22
23
  it("calls addView on the PM when subscribing", () => {
23
24
  const { mockPM, appObjects } = makeTestRig();
@@ -59,6 +60,7 @@ describe("procedureQuizResultAdapter", () => {
59
60
  authoringValidationMessage: null,
60
61
  currentStepIndex: 1,
61
62
  showHints: false,
63
+ stepCategories: { event_a: "assessed" },
62
64
  });
63
65
  expect(setVM).toHaveBeenCalledWith({
64
66
  isComplete: true,
@@ -70,6 +72,7 @@ describe("procedureQuizResultAdapter", () => {
70
72
  userEventLog: ["event_a"],
71
73
  score,
72
74
  isSubmitted: true,
75
+ stepCategories: { event_a: "assessed" },
73
76
  });
74
77
  });
75
78
  });
@@ -1 +1 @@
1
- {"version":3,"file":"procedureQuizResultAdapter.test.js","sourceRoot":"","sources":["../../../../src/Procedure/Adapters/procedureQuizResultAdapter.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { makeAppObjectRepo } from \"@vived/core\";\r\nimport { procedureQuizResultAdapter } from \"./procedureQuizResultAdapter\";\r\nimport { makeMockProcedurePM } from \"../Mocks/MockProcedurePM\";\r\n\r\nfunction makeTestRig() {\r\n\tconst appObjects = makeAppObjectRepo();\r\n\tconst mockPM = makeMockProcedurePM(appObjects);\r\n\treturn { appObjects, mockPM };\r\n}\r\n\r\ndescribe(\"procedureQuizResultAdapter\", () => {\r\n\tit(\"has the correct default VM shape\", () => {\r\n\t\tconst defaultVM = procedureQuizResultAdapter.defaultVM;\r\n\t\texpect(defaultVM.isComplete).toBe(false);\r\n\t\texpect(defaultVM.isQuiz).toBe(false);\r\n\t\texpect(defaultVM.progressPercentage).toBe(0);\r\n\t\texpect(defaultVM.eventIds).toEqual([]);\r\n\t\texpect(defaultVM.availableEvents).toEqual([]);\r\n\t\texpect(defaultVM.orderMatters).toBe(true);\r\n\t\texpect(defaultVM.userEventLog).toEqual([]);\r\n\t\texpect(defaultVM.score).toBeNull();\r\n\t\texpect(defaultVM.isSubmitted).toBe(false);\r\n\t});\r\n\r\n\tit(\"calls addView on the PM when subscribing\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.addView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\r\n\t\texpect(mockPM.addView).toHaveBeenCalled();\r\n\t});\r\n\r\n\tit(\"calls removeView on the PM when unsubscribing\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.removeView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\t\tprocedureQuizResultAdapter.unsubscribe(appObjects, setVM);\r\n\r\n\t\texpect(mockPM.removeView).toHaveBeenCalled();\r\n\t});\r\n\r\n\tit(\"maps only quiz result fields from the full VM\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.addView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\r\n\t\tconst score = {\r\n\t\t\tpercentage: 75,\r\n\t\t\tcomparison: [{ eventId: \"event_a\", expected: true, actual: true, correct: true }],\r\n\t\t};\r\n\r\n\t\tconst capturedWrapper = (mockPM.addView as jest.Mock).mock.calls[0][0];\r\n\t\tcapturedWrapper({\r\n\t\t\tisComplete: true,\r\n\t\t\tisQuiz: true,\r\n\t\t\tprogressPercentage: 75,\r\n\t\t\teventIds: [\"event_a\"],\r\n\t\t\tavailableEvents: [{ id: \"event_a\", name: \"Event A\" }],\r\n\t\t\torderMatters: false,\r\n\t\t\tuserEventLog: [\"event_a\"],\r\n\t\t\tscore,\r\n\t\t\tisSubmitted: true,\r\n\t\t\tvalidationMessage: null,\r\n\t\t\tnextExpectedEvent: null,\r\n\t\t\tauthoringValidationMessage: null,\r\n\t\t\tcurrentStepIndex: 1,\r\n\t\t\tshowHints: false,\r\n\t\t});\r\n\r\n\t\texpect(setVM).toHaveBeenCalledWith({\r\n\t\t\tisComplete: true,\r\n\t\t\tisQuiz: true,\r\n\t\t\tprogressPercentage: 75,\r\n\t\t\teventIds: [\"event_a\"],\r\n\t\t\tavailableEvents: [{ id: \"event_a\", name: \"Event A\" }],\r\n\t\t\torderMatters: false,\r\n\t\t\tuserEventLog: [\"event_a\"],\r\n\t\t\tscore,\r\n\t\t\tisSubmitted: true,\r\n\t\t});\r\n\t});\r\n});\r\n"]}
1
+ {"version":3,"file":"procedureQuizResultAdapter.test.js","sourceRoot":"","sources":["../../../../src/Procedure/Adapters/procedureQuizResultAdapter.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { makeAppObjectRepo } from \"@vived/core\";\r\nimport { procedureQuizResultAdapter } from \"./procedureQuizResultAdapter\";\r\nimport { makeMockProcedurePM } from \"../Mocks/MockProcedurePM\";\r\n\r\nfunction makeTestRig() {\r\n\tconst appObjects = makeAppObjectRepo();\r\n\tconst mockPM = makeMockProcedurePM(appObjects);\r\n\treturn { appObjects, mockPM };\r\n}\r\n\r\ndescribe(\"procedureQuizResultAdapter\", () => {\r\n\tit(\"has the correct default VM shape\", () => {\r\n\t\tconst defaultVM = procedureQuizResultAdapter.defaultVM;\r\n\t\texpect(defaultVM.isComplete).toBe(false);\r\n\t\texpect(defaultVM.isQuiz).toBe(false);\r\n\t\texpect(defaultVM.progressPercentage).toBe(0);\r\n\t\texpect(defaultVM.eventIds).toEqual([]);\r\n\t\texpect(defaultVM.availableEvents).toEqual([]);\r\n\t\texpect(defaultVM.orderMatters).toBe(true);\r\n\t\texpect(defaultVM.userEventLog).toEqual([]);\r\n\t\texpect(defaultVM.score).toBeNull();\r\n\t\texpect(defaultVM.isSubmitted).toBe(false);\r\n\t\texpect(defaultVM.stepCategories).toEqual({});\r\n\t});\r\n\r\n\tit(\"calls addView on the PM when subscribing\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.addView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\r\n\t\texpect(mockPM.addView).toHaveBeenCalled();\r\n\t});\r\n\r\n\tit(\"calls removeView on the PM when unsubscribing\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.removeView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\t\tprocedureQuizResultAdapter.unsubscribe(appObjects, setVM);\r\n\r\n\t\texpect(mockPM.removeView).toHaveBeenCalled();\r\n\t});\r\n\r\n\tit(\"maps only quiz result fields from the full VM\", () => {\r\n\t\tconst { mockPM, appObjects } = makeTestRig();\r\n\t\tmockPM.addView = jest.fn();\r\n\t\tconst setVM = jest.fn();\r\n\r\n\t\tprocedureQuizResultAdapter.subscribe(appObjects, setVM);\r\n\r\n\t\tconst score = {\r\n\t\t\tpercentage: 75,\r\n\t\t\tcomparison: [{ eventId: \"event_a\", expected: true, actual: true, correct: true }],\r\n\t\t};\r\n\r\n\t\tconst capturedWrapper = (mockPM.addView as jest.Mock).mock.calls[0][0];\r\n\t\tcapturedWrapper({\r\n\t\t\tisComplete: true,\r\n\t\t\tisQuiz: true,\r\n\t\t\tprogressPercentage: 75,\r\n\t\t\teventIds: [\"event_a\"],\r\n\t\t\tavailableEvents: [{ id: \"event_a\", name: \"Event A\" }],\r\n\t\t\torderMatters: false,\r\n\t\t\tuserEventLog: [\"event_a\"],\r\n\t\t\tscore,\r\n\t\t\tisSubmitted: true,\r\n\t\t\tvalidationMessage: null,\r\n\t\t\tnextExpectedEvent: null,\r\n\t\t\tauthoringValidationMessage: null,\r\n\t\t\tcurrentStepIndex: 1,\r\n\t\t\tshowHints: false,\r\n\t\t\tstepCategories: { event_a: \"assessed\" },\r\n\t\t});\r\n\r\n\t\texpect(setVM).toHaveBeenCalledWith({\r\n\t\t\tisComplete: true,\r\n\t\t\tisQuiz: true,\r\n\t\t\tprogressPercentage: 75,\r\n\t\t\teventIds: [\"event_a\"],\r\n\t\t\tavailableEvents: [{ id: \"event_a\", name: \"Event A\" }],\r\n\t\t\torderMatters: false,\r\n\t\t\tuserEventLog: [\"event_a\"],\r\n\t\t\tscore,\r\n\t\t\tisSubmitted: true,\r\n\t\t\tstepCategories: { event_a: \"assessed\" },\r\n\t\t});\r\n\t});\r\n});\r\n"]}
@@ -12,6 +12,55 @@ ProcedureEntity.get = (appObjects) => getSingletonComponent(ProcedureEntity.type
12
12
  export function makeProcedureEntity(appObject) {
13
13
  return new ProcedureEntityImp(appObject);
14
14
  }
15
+ /**
16
+ * Aligns `actual` against `expected` by longest common subsequence (LCS) and
17
+ * reports, for each index of `expected`, whether that step takes part in the
18
+ * alignment. `matched.filter(Boolean).length` is the LCS length.
19
+ *
20
+ * Pure function over two plain arrays: callers decide what goes in. That is the
21
+ * seam for event categories (#53) — pre-filter both arrays to the assessed
22
+ * subsequence and nothing in here needs to change.
23
+ *
24
+ * Deterministic tie-break: when several alignments share the maximum length
25
+ * (e.g. two swapped steps), the one that matches the EARLIEST expected steps
26
+ * wins, so the same step is reported incorrect on every run.
27
+ *
28
+ * O(n·m) dynamic programming; procedures are ~20 steps at most.
29
+ */
30
+ function alignBySubsequence(expected, actual) {
31
+ const n = expected.length;
32
+ const m = actual.length;
33
+ // lcs[i][j] = LCS length of expected[i..] against actual[j..]
34
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
35
+ for (let i = n - 1; i >= 0; i--) {
36
+ for (let j = m - 1; j >= 0; j--) {
37
+ lcs[i][j] =
38
+ expected[i] === actual[j]
39
+ ? lcs[i + 1][j + 1] + 1
40
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
41
+ }
42
+ }
43
+ // Walk forward from the start. Matching an equal pair is always optimal.
44
+ // On a mismatch, prefer skipping the actual entry whenever that costs
45
+ // nothing, which keeps the current (earliest) expected step in play.
46
+ const matched = new Array(n).fill(false);
47
+ let e = 0; // index into expected
48
+ let a = 0; // index into actual
49
+ while (e < n && a < m) {
50
+ if (expected[e] === actual[a]) {
51
+ matched[e] = true;
52
+ e++;
53
+ a++;
54
+ }
55
+ else if (lcs[e][a + 1] === lcs[e][a]) {
56
+ a++;
57
+ }
58
+ else {
59
+ e++;
60
+ }
61
+ }
62
+ return matched;
63
+ }
15
64
  class ProcedureEntityImp extends ProcedureEntity {
16
65
  get helpHeading() {
17
66
  if (this._isQuiz)
@@ -44,17 +93,48 @@ class ProcedureEntityImp extends ProcedureEntity {
44
93
  return this._currentStepIndex;
45
94
  }
46
95
  get isComplete() {
47
- if (this._eventIds.length === 0)
96
+ const expected = this.evaluatedEventIds;
97
+ if (expected.length === 0)
48
98
  return false;
49
99
  if (this._orderMatters) {
50
- return this._currentStepIndex >= this._eventIds.length;
100
+ return this._currentStepIndex >= expected.length;
51
101
  }
52
102
  else {
53
- const requiredEvents = new Set(this._eventIds);
54
- const emittedEvents = new Set(this._userEventLog);
103
+ const requiredEvents = new Set(expected);
104
+ const emittedEvents = new Set(this.evaluatedUserEventLog);
55
105
  return Array.from(requiredEvents).every((event) => emittedEvents.has(event));
56
106
  }
57
107
  }
108
+ /**
109
+ * Whether a step takes part in evaluation (#53). In training every authored
110
+ * step counts. In quiz mode only "assessed" steps do: instructional steps are
111
+ * quiz-neutral, so they are filtered out of BOTH the authored list and the
112
+ * student's log before any scoring, progress, completion or hint decision.
113
+ * Categories come from the injected lookup at call time, never from state.
114
+ */
115
+ isEvaluated(eventId) {
116
+ return !this._isQuiz || this._getEventCategory(eventId) === "assessed";
117
+ }
118
+ /** The authored steps that are evaluated (all of them outside quiz mode). */
119
+ get evaluatedEventIds() {
120
+ return this._eventIds.filter((eventId) => this.isEvaluated(eventId));
121
+ }
122
+ /** The student's log restricted to evaluated steps. */
123
+ get evaluatedUserEventLog() {
124
+ return this._userEventLog.filter((eventId) => this.isEvaluated(eventId));
125
+ }
126
+ /**
127
+ * The validation message for a quiz whose authored steps are all
128
+ * instructional (#53), or null when it is not that case. Such a quiz has
129
+ * events but nothing to score — scoring, progress and completion all run on
130
+ * the empty assessed subsequence — so it must be flagged rather than silently
131
+ * treated as valid.
132
+ */
133
+ get quizNeedsAssessedStepMessage() {
134
+ return this._isQuiz && this.evaluatedEventIds.length === 0
135
+ ? "A quiz needs at least one assessed step"
136
+ : null;
137
+ }
58
138
  get validationMessage() {
59
139
  return this._validationMessage;
60
140
  }
@@ -118,10 +198,30 @@ class ProcedureEntityImp extends ProcedureEntity {
118
198
  if (!this._eventIds.includes(eventId)) {
119
199
  return;
120
200
  }
201
+ if (!this.isEvaluated(eventId)) {
202
+ // Quiz mode, instructional step: quiz-neutral. Record it once as evidence
203
+ // the student reached this guidance step, but a re-emission (e.g.
204
+ // revisiting a viewpoint) is not re-logged — mirroring the benign-revisit
205
+ // rule for assessed steps and keeping the log bounded no matter how often
206
+ // the student oscillates. It never advances the attempt nor flags a wrong
207
+ // step; however they got here counts the same.
208
+ if (!this._userEventLog.includes(eventId)) {
209
+ this._userEventLog.push(eventId);
210
+ this.updateState();
211
+ }
212
+ return;
213
+ }
214
+ if (this._orderMatters && this.isBenignRevisit(eventId)) {
215
+ // Re-emission of an already-completed step (a toggle, a navigation
216
+ // arrival, an accidental double-trigger). It carries no information about
217
+ // the attempt, so it neither enters the log nor flashes a hint.
218
+ return;
219
+ }
121
220
  this._userEventLog.push(eventId);
122
221
  if (this._orderMatters) {
123
- if (this._currentStepIndex < this._eventIds.length) {
124
- const expectedEvent = this._eventIds[this._currentStepIndex];
222
+ const expected = this.evaluatedEventIds;
223
+ if (this._currentStepIndex < expected.length) {
224
+ const expectedEvent = expected[this._currentStepIndex];
125
225
  if (eventId === expectedEvent) {
126
226
  this._currentStepIndex++;
127
227
  this._validationMessage = null;
@@ -133,10 +233,25 @@ class ProcedureEntityImp extends ProcedureEntity {
133
233
  }
134
234
  this.updateState();
135
235
  }
236
+ /**
237
+ * An event is a benign revisit when it is an evaluated step that has already
238
+ * been completed. Well-defined because addEventId guarantees eventIds are
239
+ * unique, so indexOf identifies the step. An early emission of a LATER step
240
+ * is not a revisit and is still logged and flagged. `currentStepIndex`
241
+ * indexes the evaluated list, so the lookup is against that list too.
242
+ */
243
+ isBenignRevisit(eventId) {
244
+ const stepIndex = this.evaluatedEventIds.indexOf(eventId);
245
+ return stepIndex !== -1 && stepIndex < this._currentStepIndex;
246
+ }
136
247
  validateProgress() {
137
248
  if (this._eventIds.length === 0) {
138
249
  return "At least one event must be added to the procedure";
139
250
  }
251
+ const quizMessage = this.quizNeedsAssessedStepMessage;
252
+ if (quizMessage) {
253
+ return quizMessage;
254
+ }
140
255
  if (this.isComplete) {
141
256
  return null;
142
257
  }
@@ -146,17 +261,18 @@ class ProcedureEntityImp extends ProcedureEntity {
146
261
  return null;
147
262
  }
148
263
  getNextExpectedEvent() {
149
- if (!this._showHints || this._eventIds.length === 0) {
264
+ const expected = this.evaluatedEventIds;
265
+ if (!this._showHints || expected.length === 0) {
150
266
  return null;
151
267
  }
152
268
  if (this._orderMatters) {
153
- if (this._currentStepIndex < this._eventIds.length) {
154
- return this._eventIds[this._currentStepIndex];
269
+ if (this._currentStepIndex < expected.length) {
270
+ return expected[this._currentStepIndex];
155
271
  }
156
272
  }
157
273
  else {
158
- const emittedEvents = new Set(this._userEventLog);
159
- return (this._eventIds.find((eventId) => !emittedEvents.has(eventId)) || null);
274
+ const emittedEvents = new Set(this.evaluatedUserEventLog);
275
+ return expected.find((eventId) => !emittedEvents.has(eventId)) || null;
160
276
  }
161
277
  return null;
162
278
  }
@@ -187,30 +303,52 @@ class ProcedureEntityImp extends ProcedureEntity {
187
303
  this._isRecording = value;
188
304
  this.updateState();
189
305
  }
306
+ setEventCategoryLookup(lookup) {
307
+ // Wiring, not serialized state: it is not part of the state hash. Notify
308
+ // directly so a PM built before the factory wired the lookup rebuilds.
309
+ this._getEventCategory = lookup;
310
+ this.notifyOnChange();
311
+ }
312
+ getEventCategory(eventId) {
313
+ return this._getEventCategory(eventId);
314
+ }
190
315
  getProgressPercentage() {
191
- if (this._eventIds.length === 0)
316
+ const expected = this.evaluatedEventIds;
317
+ if (expected.length === 0)
192
318
  return 0;
193
319
  if (this._orderMatters) {
194
- return Math.round((this._currentStepIndex / this._eventIds.length) * 100);
320
+ return Math.round((this._currentStepIndex / expected.length) * 100);
195
321
  }
196
322
  else {
197
- const requiredEvents = new Set(this._eventIds);
198
- const emittedEvents = new Set(this._userEventLog);
323
+ const requiredEvents = new Set(expected);
324
+ const emittedEvents = new Set(this.evaluatedUserEventLog);
199
325
  const completedCount = Array.from(requiredEvents).filter((event) => emittedEvents.has(event)).length;
200
- return Math.round((completedCount / this._eventIds.length) * 100);
326
+ return Math.round((completedCount / expected.length) * 100);
201
327
  }
202
328
  }
203
329
  calculateScore() {
204
- if (this._eventIds.length === 0) {
330
+ // In quiz mode only assessed steps are scored (#53): instructional steps
331
+ // are dropped from BOTH the expected list and the log, so a quiz made only
332
+ // of instructional steps scores like an empty procedure.
333
+ const expectedIds = this.evaluatedEventIds;
334
+ if (expectedIds.length === 0) {
205
335
  return { percentage: 0, comparison: [] };
206
336
  }
207
337
  const comparison = [];
208
338
  let correctCount = 0;
209
339
  if (this._orderMatters) {
210
- for (let i = 0; i < this._eventIds.length; i++) {
211
- const eventId = this._eventIds[i];
340
+ // The score answers "did the student perform the required steps in the
341
+ // required order?" — so it is the longest run of expected steps that
342
+ // appear in order in the log, not a positional index-by-index compare
343
+ // (which collapsed to near zero after a single extra, skipped, or
344
+ // repeated step). Extra authored events are not penalized here.
345
+ // Both arrays are already the assessed subsequence in quiz mode, so an
346
+ // instructional emission can never shift the alignment.
347
+ const matched = alignBySubsequence(expectedIds, this.evaluatedUserEventLog);
348
+ for (let i = 0; i < expectedIds.length; i++) {
349
+ const eventId = expectedIds[i];
212
350
  const expected = true;
213
- const actual = i < this._userEventLog.length && this._userEventLog[i] === eventId;
351
+ const actual = matched[i];
214
352
  const correct = actual;
215
353
  comparison.push({ eventId, expected, actual, correct });
216
354
  if (correct)
@@ -218,8 +356,8 @@ class ProcedureEntityImp extends ProcedureEntity {
218
356
  }
219
357
  }
220
358
  else {
221
- const emittedEvents = new Set(this._userEventLog);
222
- for (const eventId of this._eventIds) {
359
+ const emittedEvents = new Set(this.evaluatedUserEventLog);
360
+ for (const eventId of expectedIds) {
223
361
  const expected = true;
224
362
  const actual = emittedEvents.has(eventId);
225
363
  const correct = actual;
@@ -228,7 +366,7 @@ class ProcedureEntityImp extends ProcedureEntity {
228
366
  correctCount++;
229
367
  }
230
368
  }
231
- const percentage = Math.round((correctCount / this._eventIds.length) * 100);
369
+ const percentage = Math.round((correctCount / expectedIds.length) * 100);
232
370
  return { percentage, comparison };
233
371
  }
234
372
  submit() {
@@ -246,7 +384,7 @@ class ProcedureEntityImp extends ProcedureEntity {
246
384
  if (this._eventIds.length === 0) {
247
385
  return "At least one event must be added to the procedure";
248
386
  }
249
- return this._validationMessage;
387
+ return this.quizNeedsAssessedStepMessage ?? this._validationMessage;
250
388
  }
251
389
  updateState() {
252
390
  const stateHash = JSON.stringify({
@@ -275,6 +413,7 @@ class ProcedureEntityImp extends ProcedureEntity {
275
413
  this._validationMessage = null;
276
414
  this._score = null;
277
415
  this._isSubmitted = false;
416
+ this._getEventCategory = () => "assessed";
278
417
  this.memoizedState = new MemoizedString("", this.notifyOnChange);
279
418
  this.appObjects.registerSingleton(this);
280
419
  this.updateState();
@@ -1 +1 @@
1
- {"version":3,"file":"ProcedureEntity.js","sourceRoot":"","sources":["../../../../src/Procedure/Entities/ProcedureEntity.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n AppObject,\r\n AppObjectEntity,\r\n AppObjectRepo,\r\n getSingletonComponent,\r\n MemoizedString,\r\n} from \"@vived/core\";\r\nimport { Challenge } from \"../../Challenge\";\r\n\r\nexport abstract class ProcedureEntity\r\n extends AppObjectEntity\r\n implements Challenge\r\n{\r\n static readonly type = \"ProcedureEntity\";\r\n\r\n readonly challengeID = \"PROCEDURE\";\r\n name = \"Procedure\";\r\n abstract get helpHeading(): string;\r\n abstract get consumerHelpText(): string;\r\n authorHelpText =\r\n \"Create step-by-step procedures by selecting events that users must emit.\";\r\n\r\n abstract get eventIds(): string[];\r\n abstract get orderMatters(): boolean;\r\n abstract get showHints(): boolean;\r\n abstract get isQuiz(): boolean;\r\n abstract get userEventLog(): string[];\r\n abstract get currentStepIndex(): number;\r\n abstract get isComplete(): boolean;\r\n abstract get validationMessage(): string | null;\r\n abstract get score(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null;\r\n abstract get isSubmitted(): boolean;\r\n\r\n abstract addEventId(eventId: string): void;\r\n abstract removeEventId(eventId: string): void;\r\n abstract clearEventIds(): void;\r\n abstract recordUserEvent(eventId: string): void;\r\n abstract validateProgress(): string | null;\r\n abstract getNextExpectedEvent(): string | null;\r\n abstract reset(): void;\r\n abstract moveEventUp(index: number): void;\r\n abstract moveEventDown(index: number): void;\r\n abstract getAuthoringValidationMessage(): string | null;\r\n\r\n abstract setOrderMatters(orderMatters: boolean): void;\r\n abstract setShowHints(showHints: boolean): void;\r\n abstract setIsQuiz(isQuiz: boolean): void;\r\n\r\n abstract get isRecording(): boolean;\r\n abstract setIsRecording(value: boolean): void;\r\n\r\n abstract getProgressPercentage(): number;\r\n abstract calculateScore(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n };\r\n abstract submit(): void;\r\n\r\n static get = (appObjects: AppObjectRepo): ProcedureEntity | undefined =>\r\n getSingletonComponent(ProcedureEntity.type, appObjects);\r\n}\r\n\r\nexport function makeProcedureEntity(appObject: AppObject): ProcedureEntity {\r\n return new ProcedureEntityImp(appObject);\r\n}\r\n\r\nclass ProcedureEntityImp extends ProcedureEntity {\r\n private _eventIds: string[] = [];\r\n private _orderMatters = true;\r\n private _showHints = true;\r\n private _isQuiz = false;\r\n private _isRecording = false;\r\n private _userEventLog: string[] = [];\r\n private _currentStepIndex = 0;\r\n private _validationMessage: string | null = null;\r\n private _score: {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null = null;\r\n private _isSubmitted = false;\r\n\r\n get helpHeading(): string {\r\n if (this._isQuiz) return \"Procedure Challenge\";\r\n else return \"Procedure Training\";\r\n }\r\n\r\n get consumerHelpText(): string {\r\n if (this._isQuiz)\r\n return \"Complete the procedure challenge on your own and get scored after submitting.\";\r\n else return \"Follow the steps in order to complete the training procedure.\";\r\n }\r\n\r\n private memoizedState = new MemoizedString(\"\", this.notifyOnChange);\r\n\r\n get eventIds(): string[] {\r\n return [...this._eventIds];\r\n }\r\n\r\n get orderMatters(): boolean {\r\n return this._orderMatters;\r\n }\r\n\r\n get showHints(): boolean {\r\n return this._showHints;\r\n }\r\n\r\n get isQuiz(): boolean {\r\n return this._isQuiz;\r\n }\r\n\r\n get userEventLog(): string[] {\r\n return [...this._userEventLog];\r\n }\r\n\r\n get currentStepIndex(): number {\r\n return this._currentStepIndex;\r\n }\r\n\r\n get isComplete(): boolean {\r\n if (this._eventIds.length === 0) return false;\r\n\r\n if (this._orderMatters) {\r\n return this._currentStepIndex >= this._eventIds.length;\r\n } else {\r\n const requiredEvents = new Set(this._eventIds);\r\n const emittedEvents = new Set(this._userEventLog);\r\n return Array.from(requiredEvents).every((event) =>\r\n emittedEvents.has(event),\r\n );\r\n }\r\n }\r\n\r\n get validationMessage(): string | null {\r\n return this._validationMessage;\r\n }\r\n\r\n get score(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null {\r\n return this._score;\r\n }\r\n\r\n get isSubmitted(): boolean {\r\n return this._isSubmitted;\r\n }\r\n\r\n addEventId(eventId: string): void {\r\n if (this._eventIds.includes(eventId)) {\r\n this._validationMessage = `Event '${eventId}' is already in the procedure`;\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._eventIds.push(eventId);\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n removeEventId(eventId: string): void {\r\n const index = this._eventIds.indexOf(eventId);\r\n if (index === -1) {\r\n this._validationMessage = `Event '${eventId}' is not in the procedure`;\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._eventIds.splice(index, 1);\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n clearEventIds(): void {\r\n this._eventIds = [];\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n moveEventUp(index: number): void {\r\n if (index <= 0 || index >= this._eventIds.length) {\r\n this._validationMessage = \"Cannot move event up - invalid position\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n const temp = this._eventIds[index];\r\n this._eventIds[index] = this._eventIds[index - 1];\r\n this._eventIds[index - 1] = temp;\r\n\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n moveEventDown(index: number): void {\r\n if (index < 0 || index >= this._eventIds.length - 1) {\r\n this._validationMessage = \"Cannot move event down - invalid position\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n const temp = this._eventIds[index];\r\n this._eventIds[index] = this._eventIds[index + 1];\r\n this._eventIds[index + 1] = temp;\r\n\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n recordUserEvent(eventId: string): void {\r\n if (!this._eventIds.includes(eventId)) {\r\n return;\r\n }\r\n\r\n this._userEventLog.push(eventId);\r\n\r\n if (this._orderMatters) {\r\n if (this._currentStepIndex < this._eventIds.length) {\r\n const expectedEvent = this._eventIds[this._currentStepIndex];\r\n if (eventId === expectedEvent) {\r\n this._currentStepIndex++;\r\n this._validationMessage = null;\r\n } else {\r\n this._validationMessage = `Expected '${expectedEvent}', but got '${eventId}'`;\r\n }\r\n }\r\n }\r\n\r\n this.updateState();\r\n }\r\n\r\n validateProgress(): string | null {\r\n if (this._eventIds.length === 0) {\r\n return \"At least one event must be added to the procedure\";\r\n }\r\n\r\n if (this.isComplete) {\r\n return null;\r\n }\r\n\r\n if (this._orderMatters && this._validationMessage) {\r\n return this._validationMessage;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n getNextExpectedEvent(): string | null {\r\n if (!this._showHints || this._eventIds.length === 0) {\r\n return null;\r\n }\r\n\r\n if (this._orderMatters) {\r\n if (this._currentStepIndex < this._eventIds.length) {\r\n return this._eventIds[this._currentStepIndex];\r\n }\r\n } else {\r\n const emittedEvents = new Set(this._userEventLog);\r\n return (\r\n this._eventIds.find((eventId) => !emittedEvents.has(eventId)) || null\r\n );\r\n }\r\n\r\n return null;\r\n }\r\n\r\n reset(): void {\r\n this._userEventLog = [];\r\n this._currentStepIndex = 0;\r\n this._validationMessage = null;\r\n this._score = null;\r\n this._isSubmitted = false;\r\n this.updateState();\r\n }\r\n\r\n setOrderMatters(orderMatters: boolean): void {\r\n this._orderMatters = orderMatters;\r\n this.updateState();\r\n }\r\n\r\n setShowHints(showHints: boolean): void {\r\n this._showHints = showHints;\r\n this.updateState();\r\n }\r\n\r\n setIsQuiz(isQuiz: boolean): void {\r\n this._isQuiz = isQuiz;\r\n this.updateState();\r\n }\r\n\r\n get isRecording(): boolean {\r\n return this._isRecording;\r\n }\r\n\r\n setIsRecording(value: boolean): void {\r\n this._isRecording = value;\r\n this.updateState();\r\n }\r\n\r\n getProgressPercentage(): number {\r\n if (this._eventIds.length === 0) return 0;\r\n\r\n if (this._orderMatters) {\r\n return Math.round((this._currentStepIndex / this._eventIds.length) * 100);\r\n } else {\r\n const requiredEvents = new Set(this._eventIds);\r\n const emittedEvents = new Set(this._userEventLog);\r\n const completedCount = Array.from(requiredEvents).filter((event) =>\r\n emittedEvents.has(event),\r\n ).length;\r\n return Math.round((completedCount / this._eventIds.length) * 100);\r\n }\r\n }\r\n\r\n calculateScore(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } {\r\n if (this._eventIds.length === 0) {\r\n return { percentage: 0, comparison: [] };\r\n }\r\n\r\n const comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[] = [];\r\n let correctCount = 0;\r\n\r\n if (this._orderMatters) {\r\n for (let i = 0; i < this._eventIds.length; i++) {\r\n const eventId = this._eventIds[i];\r\n const expected = true;\r\n const actual =\r\n i < this._userEventLog.length && this._userEventLog[i] === eventId;\r\n const correct = actual;\r\n\r\n comparison.push({ eventId, expected, actual, correct });\r\n if (correct) correctCount++;\r\n }\r\n } else {\r\n const emittedEvents = new Set(this._userEventLog);\r\n for (const eventId of this._eventIds) {\r\n const expected = true;\r\n const actual = emittedEvents.has(eventId);\r\n const correct = actual;\r\n\r\n comparison.push({ eventId, expected, actual, correct });\r\n if (correct) correctCount++;\r\n }\r\n }\r\n\r\n const percentage = Math.round(\r\n (correctCount / this._eventIds.length) * 100,\r\n );\r\n return { percentage, comparison };\r\n }\r\n\r\n submit(): void {\r\n if (!this._isQuiz) {\r\n this._validationMessage = \"Cannot submit: not in quiz mode\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._score = this.calculateScore();\r\n this._isSubmitted = true;\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n getAuthoringValidationMessage(): string | null {\r\n if (this._eventIds.length === 0) {\r\n return \"At least one event must be added to the procedure\";\r\n }\r\n return this._validationMessage;\r\n }\r\n\r\n private updateState(): void {\r\n const stateHash = JSON.stringify({\r\n eventIds: this._eventIds,\r\n orderMatters: this._orderMatters,\r\n showHints: this._showHints,\r\n isQuiz: this._isQuiz,\r\n userEventLog: this._userEventLog,\r\n currentStepIndex: this._currentStepIndex,\r\n validationMessage: this._validationMessage,\r\n score: this._score,\r\n isSubmitted: this._isSubmitted,\r\n isRecording: this._isRecording,\r\n });\r\n\r\n this.memoizedState.val = stateHash;\r\n }\r\n\r\n constructor(appObject: AppObject) {\r\n super(appObject, ProcedureEntity.type);\r\n this.appObjects.registerSingleton(this);\r\n this.updateState();\r\n }\r\n}\r\n"]}
1
+ {"version":3,"file":"ProcedureEntity.js","sourceRoot":"","sources":["../../../../src/Procedure/Entities/ProcedureEntity.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n AppObject,\r\n AppObjectEntity,\r\n AppObjectRepo,\r\n getSingletonComponent,\r\n MemoizedString,\r\n} from \"@vived/core\";\r\nimport { Challenge } from \"../../Challenge\";\r\nimport { EventCategory } from \"../../EventSystem/types\";\r\nimport { EventCategoryLookup } from \"../types\";\r\n\r\nexport abstract class ProcedureEntity\r\n extends AppObjectEntity\r\n implements Challenge\r\n{\r\n static readonly type = \"ProcedureEntity\";\r\n\r\n readonly challengeID = \"PROCEDURE\";\r\n name = \"Procedure\";\r\n abstract get helpHeading(): string;\r\n abstract get consumerHelpText(): string;\r\n authorHelpText =\r\n \"Create step-by-step procedures by selecting events that users must emit.\";\r\n\r\n abstract get eventIds(): string[];\r\n abstract get orderMatters(): boolean;\r\n abstract get showHints(): boolean;\r\n abstract get isQuiz(): boolean;\r\n abstract get userEventLog(): string[];\r\n abstract get currentStepIndex(): number;\r\n abstract get isComplete(): boolean;\r\n abstract get validationMessage(): string | null;\r\n abstract get score(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null;\r\n abstract get isSubmitted(): boolean;\r\n\r\n abstract addEventId(eventId: string): void;\r\n abstract removeEventId(eventId: string): void;\r\n abstract clearEventIds(): void;\r\n abstract recordUserEvent(eventId: string): void;\r\n abstract validateProgress(): string | null;\r\n abstract getNextExpectedEvent(): string | null;\r\n abstract reset(): void;\r\n abstract moveEventUp(index: number): void;\r\n abstract moveEventDown(index: number): void;\r\n abstract getAuthoringValidationMessage(): string | null;\r\n\r\n abstract setOrderMatters(orderMatters: boolean): void;\r\n abstract setShowHints(showHints: boolean): void;\r\n abstract setIsQuiz(isQuiz: boolean): void;\r\n\r\n abstract get isRecording(): boolean;\r\n abstract setIsRecording(value: boolean): void;\r\n\r\n /**\r\n * Inject the Registry lookup used to classify steps (#53). Wired by the\r\n * FeatureFactory; evaluated at call time so late registrations are seen.\r\n * With no lookup injected every step is \"assessed\".\r\n */\r\n abstract setEventCategoryLookup(lookup: EventCategoryLookup): void;\r\n /** The category of an authored (or any) event id, via the injected lookup. */\r\n abstract getEventCategory(eventId: string): EventCategory;\r\n\r\n abstract getProgressPercentage(): number;\r\n abstract calculateScore(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n };\r\n abstract submit(): void;\r\n\r\n static get = (appObjects: AppObjectRepo): ProcedureEntity | undefined =>\r\n getSingletonComponent(ProcedureEntity.type, appObjects);\r\n}\r\n\r\nexport function makeProcedureEntity(appObject: AppObject): ProcedureEntity {\r\n return new ProcedureEntityImp(appObject);\r\n}\r\n\r\n/**\r\n * Aligns `actual` against `expected` by longest common subsequence (LCS) and\r\n * reports, for each index of `expected`, whether that step takes part in the\r\n * alignment. `matched.filter(Boolean).length` is the LCS length.\r\n *\r\n * Pure function over two plain arrays: callers decide what goes in. That is the\r\n * seam for event categories (#53) — pre-filter both arrays to the assessed\r\n * subsequence and nothing in here needs to change.\r\n *\r\n * Deterministic tie-break: when several alignments share the maximum length\r\n * (e.g. two swapped steps), the one that matches the EARLIEST expected steps\r\n * wins, so the same step is reported incorrect on every run.\r\n *\r\n * O(n·m) dynamic programming; procedures are ~20 steps at most.\r\n */\r\nfunction alignBySubsequence(\r\n expected: string[],\r\n actual: string[],\r\n): boolean[] {\r\n const n = expected.length;\r\n const m = actual.length;\r\n\r\n // lcs[i][j] = LCS length of expected[i..] against actual[j..]\r\n const lcs: number[][] = Array.from({ length: n + 1 }, () =>\r\n new Array<number>(m + 1).fill(0),\r\n );\r\n for (let i = n - 1; i >= 0; i--) {\r\n for (let j = m - 1; j >= 0; j--) {\r\n lcs[i][j] =\r\n expected[i] === actual[j]\r\n ? lcs[i + 1][j + 1] + 1\r\n : Math.max(lcs[i + 1][j], lcs[i][j + 1]);\r\n }\r\n }\r\n\r\n // Walk forward from the start. Matching an equal pair is always optimal.\r\n // On a mismatch, prefer skipping the actual entry whenever that costs\r\n // nothing, which keeps the current (earliest) expected step in play.\r\n const matched = new Array<boolean>(n).fill(false);\r\n let e = 0; // index into expected\r\n let a = 0; // index into actual\r\n while (e < n && a < m) {\r\n if (expected[e] === actual[a]) {\r\n matched[e] = true;\r\n e++;\r\n a++;\r\n } else if (lcs[e][a + 1] === lcs[e][a]) {\r\n a++;\r\n } else {\r\n e++;\r\n }\r\n }\r\n return matched;\r\n}\r\n\r\nclass ProcedureEntityImp extends ProcedureEntity {\r\n private _eventIds: string[] = [];\r\n private _orderMatters = true;\r\n private _showHints = true;\r\n private _isQuiz = false;\r\n private _isRecording = false;\r\n private _userEventLog: string[] = [];\r\n private _currentStepIndex = 0;\r\n private _validationMessage: string | null = null;\r\n private _score: {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null = null;\r\n private _isSubmitted = false;\r\n private _getEventCategory: EventCategoryLookup = () => \"assessed\";\r\n\r\n get helpHeading(): string {\r\n if (this._isQuiz) return \"Procedure Challenge\";\r\n else return \"Procedure Training\";\r\n }\r\n\r\n get consumerHelpText(): string {\r\n if (this._isQuiz)\r\n return \"Complete the procedure challenge on your own and get scored after submitting.\";\r\n else return \"Follow the steps in order to complete the training procedure.\";\r\n }\r\n\r\n private memoizedState = new MemoizedString(\"\", this.notifyOnChange);\r\n\r\n get eventIds(): string[] {\r\n return [...this._eventIds];\r\n }\r\n\r\n get orderMatters(): boolean {\r\n return this._orderMatters;\r\n }\r\n\r\n get showHints(): boolean {\r\n return this._showHints;\r\n }\r\n\r\n get isQuiz(): boolean {\r\n return this._isQuiz;\r\n }\r\n\r\n get userEventLog(): string[] {\r\n return [...this._userEventLog];\r\n }\r\n\r\n get currentStepIndex(): number {\r\n return this._currentStepIndex;\r\n }\r\n\r\n get isComplete(): boolean {\r\n const expected = this.evaluatedEventIds;\r\n if (expected.length === 0) return false;\r\n\r\n if (this._orderMatters) {\r\n return this._currentStepIndex >= expected.length;\r\n } else {\r\n const requiredEvents = new Set(expected);\r\n const emittedEvents = new Set(this.evaluatedUserEventLog);\r\n return Array.from(requiredEvents).every((event) =>\r\n emittedEvents.has(event),\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Whether a step takes part in evaluation (#53). In training every authored\r\n * step counts. In quiz mode only \"assessed\" steps do: instructional steps are\r\n * quiz-neutral, so they are filtered out of BOTH the authored list and the\r\n * student's log before any scoring, progress, completion or hint decision.\r\n * Categories come from the injected lookup at call time, never from state.\r\n */\r\n private isEvaluated(eventId: string): boolean {\r\n return !this._isQuiz || this._getEventCategory(eventId) === \"assessed\";\r\n }\r\n\r\n /** The authored steps that are evaluated (all of them outside quiz mode). */\r\n private get evaluatedEventIds(): string[] {\r\n return this._eventIds.filter((eventId) => this.isEvaluated(eventId));\r\n }\r\n\r\n /** The student's log restricted to evaluated steps. */\r\n private get evaluatedUserEventLog(): string[] {\r\n return this._userEventLog.filter((eventId) => this.isEvaluated(eventId));\r\n }\r\n\r\n /**\r\n * The validation message for a quiz whose authored steps are all\r\n * instructional (#53), or null when it is not that case. Such a quiz has\r\n * events but nothing to score — scoring, progress and completion all run on\r\n * the empty assessed subsequence — so it must be flagged rather than silently\r\n * treated as valid.\r\n */\r\n private get quizNeedsAssessedStepMessage(): string | null {\r\n return this._isQuiz && this.evaluatedEventIds.length === 0\r\n ? \"A quiz needs at least one assessed step\"\r\n : null;\r\n }\r\n\r\n get validationMessage(): string | null {\r\n return this._validationMessage;\r\n }\r\n\r\n get score(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } | null {\r\n return this._score;\r\n }\r\n\r\n get isSubmitted(): boolean {\r\n return this._isSubmitted;\r\n }\r\n\r\n addEventId(eventId: string): void {\r\n if (this._eventIds.includes(eventId)) {\r\n this._validationMessage = `Event '${eventId}' is already in the procedure`;\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._eventIds.push(eventId);\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n removeEventId(eventId: string): void {\r\n const index = this._eventIds.indexOf(eventId);\r\n if (index === -1) {\r\n this._validationMessage = `Event '${eventId}' is not in the procedure`;\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._eventIds.splice(index, 1);\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n clearEventIds(): void {\r\n this._eventIds = [];\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n moveEventUp(index: number): void {\r\n if (index <= 0 || index >= this._eventIds.length) {\r\n this._validationMessage = \"Cannot move event up - invalid position\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n const temp = this._eventIds[index];\r\n this._eventIds[index] = this._eventIds[index - 1];\r\n this._eventIds[index - 1] = temp;\r\n\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n moveEventDown(index: number): void {\r\n if (index < 0 || index >= this._eventIds.length - 1) {\r\n this._validationMessage = \"Cannot move event down - invalid position\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n const temp = this._eventIds[index];\r\n this._eventIds[index] = this._eventIds[index + 1];\r\n this._eventIds[index + 1] = temp;\r\n\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n recordUserEvent(eventId: string): void {\r\n if (!this._eventIds.includes(eventId)) {\r\n return;\r\n }\r\n\r\n if (!this.isEvaluated(eventId)) {\r\n // Quiz mode, instructional step: quiz-neutral. Record it once as evidence\r\n // the student reached this guidance step, but a re-emission (e.g.\r\n // revisiting a viewpoint) is not re-logged — mirroring the benign-revisit\r\n // rule for assessed steps and keeping the log bounded no matter how often\r\n // the student oscillates. It never advances the attempt nor flags a wrong\r\n // step; however they got here counts the same.\r\n if (!this._userEventLog.includes(eventId)) {\r\n this._userEventLog.push(eventId);\r\n this.updateState();\r\n }\r\n return;\r\n }\r\n\r\n if (this._orderMatters && this.isBenignRevisit(eventId)) {\r\n // Re-emission of an already-completed step (a toggle, a navigation\r\n // arrival, an accidental double-trigger). It carries no information about\r\n // the attempt, so it neither enters the log nor flashes a hint.\r\n return;\r\n }\r\n\r\n this._userEventLog.push(eventId);\r\n\r\n if (this._orderMatters) {\r\n const expected = this.evaluatedEventIds;\r\n if (this._currentStepIndex < expected.length) {\r\n const expectedEvent = expected[this._currentStepIndex];\r\n if (eventId === expectedEvent) {\r\n this._currentStepIndex++;\r\n this._validationMessage = null;\r\n } else {\r\n this._validationMessage = `Expected '${expectedEvent}', but got '${eventId}'`;\r\n }\r\n }\r\n }\r\n\r\n this.updateState();\r\n }\r\n\r\n /**\r\n * An event is a benign revisit when it is an evaluated step that has already\r\n * been completed. Well-defined because addEventId guarantees eventIds are\r\n * unique, so indexOf identifies the step. An early emission of a LATER step\r\n * is not a revisit and is still logged and flagged. `currentStepIndex`\r\n * indexes the evaluated list, so the lookup is against that list too.\r\n */\r\n private isBenignRevisit(eventId: string): boolean {\r\n const stepIndex = this.evaluatedEventIds.indexOf(eventId);\r\n return stepIndex !== -1 && stepIndex < this._currentStepIndex;\r\n }\r\n\r\n validateProgress(): string | null {\r\n if (this._eventIds.length === 0) {\r\n return \"At least one event must be added to the procedure\";\r\n }\r\n\r\n const quizMessage = this.quizNeedsAssessedStepMessage;\r\n if (quizMessage) {\r\n return quizMessage;\r\n }\r\n\r\n if (this.isComplete) {\r\n return null;\r\n }\r\n\r\n if (this._orderMatters && this._validationMessage) {\r\n return this._validationMessage;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n getNextExpectedEvent(): string | null {\r\n const expected = this.evaluatedEventIds;\r\n if (!this._showHints || expected.length === 0) {\r\n return null;\r\n }\r\n\r\n if (this._orderMatters) {\r\n if (this._currentStepIndex < expected.length) {\r\n return expected[this._currentStepIndex];\r\n }\r\n } else {\r\n const emittedEvents = new Set(this.evaluatedUserEventLog);\r\n return expected.find((eventId) => !emittedEvents.has(eventId)) || null;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n reset(): void {\r\n this._userEventLog = [];\r\n this._currentStepIndex = 0;\r\n this._validationMessage = null;\r\n this._score = null;\r\n this._isSubmitted = false;\r\n this.updateState();\r\n }\r\n\r\n setOrderMatters(orderMatters: boolean): void {\r\n this._orderMatters = orderMatters;\r\n this.updateState();\r\n }\r\n\r\n setShowHints(showHints: boolean): void {\r\n this._showHints = showHints;\r\n this.updateState();\r\n }\r\n\r\n setIsQuiz(isQuiz: boolean): void {\r\n this._isQuiz = isQuiz;\r\n this.updateState();\r\n }\r\n\r\n get isRecording(): boolean {\r\n return this._isRecording;\r\n }\r\n\r\n setIsRecording(value: boolean): void {\r\n this._isRecording = value;\r\n this.updateState();\r\n }\r\n\r\n setEventCategoryLookup(lookup: EventCategoryLookup): void {\r\n // Wiring, not serialized state: it is not part of the state hash. Notify\r\n // directly so a PM built before the factory wired the lookup rebuilds.\r\n this._getEventCategory = lookup;\r\n this.notifyOnChange();\r\n }\r\n\r\n getEventCategory(eventId: string): EventCategory {\r\n return this._getEventCategory(eventId);\r\n }\r\n\r\n getProgressPercentage(): number {\r\n const expected = this.evaluatedEventIds;\r\n if (expected.length === 0) return 0;\r\n\r\n if (this._orderMatters) {\r\n return Math.round((this._currentStepIndex / expected.length) * 100);\r\n } else {\r\n const requiredEvents = new Set(expected);\r\n const emittedEvents = new Set(this.evaluatedUserEventLog);\r\n const completedCount = Array.from(requiredEvents).filter((event) =>\r\n emittedEvents.has(event),\r\n ).length;\r\n return Math.round((completedCount / expected.length) * 100);\r\n }\r\n }\r\n\r\n calculateScore(): {\r\n percentage: number;\r\n comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[];\r\n } {\r\n // In quiz mode only assessed steps are scored (#53): instructional steps\r\n // are dropped from BOTH the expected list and the log, so a quiz made only\r\n // of instructional steps scores like an empty procedure.\r\n const expectedIds = this.evaluatedEventIds;\r\n if (expectedIds.length === 0) {\r\n return { percentage: 0, comparison: [] };\r\n }\r\n\r\n const comparison: {eventId: string; expected: boolean; actual: boolean; correct: boolean}[] = [];\r\n let correctCount = 0;\r\n\r\n if (this._orderMatters) {\r\n // The score answers \"did the student perform the required steps in the\r\n // required order?\" — so it is the longest run of expected steps that\r\n // appear in order in the log, not a positional index-by-index compare\r\n // (which collapsed to near zero after a single extra, skipped, or\r\n // repeated step). Extra authored events are not penalized here.\r\n // Both arrays are already the assessed subsequence in quiz mode, so an\r\n // instructional emission can never shift the alignment.\r\n const matched = alignBySubsequence(expectedIds, this.evaluatedUserEventLog);\r\n for (let i = 0; i < expectedIds.length; i++) {\r\n const eventId = expectedIds[i];\r\n const expected = true;\r\n const actual = matched[i];\r\n const correct = actual;\r\n\r\n comparison.push({ eventId, expected, actual, correct });\r\n if (correct) correctCount++;\r\n }\r\n } else {\r\n const emittedEvents = new Set(this.evaluatedUserEventLog);\r\n for (const eventId of expectedIds) {\r\n const expected = true;\r\n const actual = emittedEvents.has(eventId);\r\n const correct = actual;\r\n\r\n comparison.push({ eventId, expected, actual, correct });\r\n if (correct) correctCount++;\r\n }\r\n }\r\n\r\n const percentage = Math.round((correctCount / expectedIds.length) * 100);\r\n return { percentage, comparison };\r\n }\r\n\r\n submit(): void {\r\n if (!this._isQuiz) {\r\n this._validationMessage = \"Cannot submit: not in quiz mode\";\r\n this.updateState();\r\n return;\r\n }\r\n\r\n this._score = this.calculateScore();\r\n this._isSubmitted = true;\r\n this._validationMessage = null;\r\n this.updateState();\r\n }\r\n\r\n getAuthoringValidationMessage(): string | null {\r\n if (this._eventIds.length === 0) {\r\n return \"At least one event must be added to the procedure\";\r\n }\r\n return this.quizNeedsAssessedStepMessage ?? this._validationMessage;\r\n }\r\n\r\n private updateState(): void {\r\n const stateHash = JSON.stringify({\r\n eventIds: this._eventIds,\r\n orderMatters: this._orderMatters,\r\n showHints: this._showHints,\r\n isQuiz: this._isQuiz,\r\n userEventLog: this._userEventLog,\r\n currentStepIndex: this._currentStepIndex,\r\n validationMessage: this._validationMessage,\r\n score: this._score,\r\n isSubmitted: this._isSubmitted,\r\n isRecording: this._isRecording,\r\n });\r\n\r\n this.memoizedState.val = stateHash;\r\n }\r\n\r\n constructor(appObject: AppObject) {\r\n super(appObject, ProcedureEntity.type);\r\n this.appObjects.registerSingleton(this);\r\n this.updateState();\r\n }\r\n}\r\n"]}