@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75

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 (274) hide show
  1. package/README.md +469 -318
  2. package/dist/attempt/AssessmentSession.d.ts +1 -5
  3. package/dist/attempt/AssessmentSession.js +0 -29
  4. package/dist/attempt/TestSession.d.ts +1 -20
  5. package/dist/attempt/TestSession.js +8 -99
  6. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  7. package/dist/components/ItemScope.custom-element.js +1 -0
  8. package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
  9. package/dist/components/ItemToolBar.custom-element.js +1 -1
  10. package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
  12. package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
  13. package/dist/components/SectionToolBar.custom-element.js +1 -1
  14. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  15. package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
  16. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  17. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  18. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  19. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  20. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  21. package/dist/components/item-scope-element.d.ts +2 -0
  22. package/dist/components/item-scope-element.js +2 -0
  23. package/dist/context/assessment-toolkit-context.d.ts +32 -6
  24. package/dist/index.d.ts +23 -48
  25. package/dist/index.js +12 -37
  26. package/dist/policy/core/PolicySource.d.ts +1 -1
  27. package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
  28. package/dist/policy/core/ToolPolicyEngine.js +148 -46
  29. package/dist/policy/core/compose-decision.d.ts +28 -7
  30. package/dist/policy/core/compose-decision.js +75 -54
  31. package/dist/policy/core/decision-types.d.ts +65 -34
  32. package/dist/policy/core/feature-decision.d.ts +16 -9
  33. package/dist/policy/core/feature-decision.js +14 -8
  34. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  35. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  36. package/dist/policy/core/provenance.d.ts +9 -4
  37. package/dist/policy/core/provenance.js +1 -1
  38. package/dist/policy/engine.d.ts +7 -23
  39. package/dist/policy/engine.js +5 -20
  40. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  41. package/dist/policy/sources/PnpPolicySource.js +117 -118
  42. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  43. package/dist/runtime/SectionControllerBinding.js +450 -0
  44. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  45. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  46. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  47. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  48. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  49. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  50. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  51. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  52. package/dist/runtime/catalog-context.d.ts +19 -0
  53. package/dist/runtime/catalog-context.js +28 -0
  54. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  55. package/dist/runtime/composition-emit-scheduler.js +9 -15
  56. package/dist/runtime/content-language.d.ts +32 -0
  57. package/dist/runtime/content-language.js +46 -0
  58. package/dist/runtime/content-region.d.ts +17 -0
  59. package/dist/runtime/content-region.js +29 -0
  60. package/dist/runtime/content-registration.d.ts +13 -0
  61. package/dist/runtime/content-registration.js +25 -0
  62. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  63. package/dist/runtime/core/SectionEngineCore.js +2 -2
  64. package/dist/runtime/core/cohort.d.ts +1 -1
  65. package/dist/runtime/core/cohort.js +1 -1
  66. package/dist/runtime/core/engine-input.d.ts +18 -30
  67. package/dist/runtime/core/engine-input.js +11 -12
  68. package/dist/runtime/core/engine-output.d.ts +16 -40
  69. package/dist/runtime/core/engine-output.js +9 -21
  70. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  71. package/dist/runtime/core/engine-readiness.js +8 -41
  72. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  73. package/dist/runtime/core/engine-resolver.js +18 -43
  74. package/dist/runtime/core/engine-state.d.ts +28 -36
  75. package/dist/runtime/core/engine-state.js +18 -21
  76. package/dist/runtime/core/engine-transition.d.ts +14 -19
  77. package/dist/runtime/core/engine-transition.js +71 -51
  78. package/dist/runtime/engine.d.ts +16 -19
  79. package/dist/runtime/engine.js +12 -19
  80. package/dist/runtime/page-warnings.d.ts +13 -0
  81. package/dist/runtime/page-warnings.js +22 -0
  82. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  83. package/dist/runtime/policy-input-forwarding.js +25 -0
  84. package/dist/runtime/registration-events.d.ts +14 -0
  85. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  86. package/dist/runtime/runtime-event-claim.js +15 -0
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +188 -0
  93. package/dist/runtime/tool-host-contract.d.ts +28 -10
  94. package/dist/runtime/tool-host-contract.js +31 -10
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/ElementToolStateStore.d.ts +14 -10
  98. package/dist/services/ElementToolStateStore.js +37 -13
  99. package/dist/services/HighlightCoordinator.d.ts +22 -28
  100. package/dist/services/HighlightCoordinator.js +431 -327
  101. package/dist/services/RangeSerializer.d.ts +10 -1
  102. package/dist/services/RangeSerializer.js +87 -14
  103. package/dist/services/TTSService.d.ts +130 -69
  104. package/dist/services/TTSService.js +713 -570
  105. package/dist/services/ToolCoordinator.d.ts +43 -66
  106. package/dist/services/ToolCoordinator.js +164 -208
  107. package/dist/services/ToolRegistry.d.ts +78 -54
  108. package/dist/services/ToolRegistry.js +107 -105
  109. package/dist/services/ToolkitCoordinator.d.ts +261 -175
  110. package/dist/services/ToolkitCoordinator.js +667 -433
  111. package/dist/services/audio-handoff.d.ts +6 -7
  112. package/dist/services/audio-handoff.js +20 -14
  113. package/dist/services/catalog-owner.d.ts +11 -1
  114. package/dist/services/catalog-owner.js +25 -0
  115. package/dist/services/create-tools-config.d.ts +2 -2
  116. package/dist/services/framework-error-bus.d.ts +3 -6
  117. package/dist/services/framework-error-bus.js +3 -6
  118. package/dist/services/framework-error.d.ts +30 -8
  119. package/dist/services/framework-error.js +9 -7
  120. package/dist/services/interfaces.d.ts +158 -129
  121. package/dist/services/section-controller-types.d.ts +15 -0
  122. package/dist/services/section-session-assignment.d.ts +21 -0
  123. package/dist/services/section-session-assignment.js +82 -0
  124. package/dist/services/spoken-audio-cards.d.ts +0 -2
  125. package/dist/services/spoken-audio-cards.js +1 -3
  126. package/dist/services/tool-config-defaults.d.ts +3 -3
  127. package/dist/services/tool-config-defaults.js +3 -3
  128. package/dist/services/tool-config-validation.d.ts +14 -4
  129. package/dist/services/tool-config-validation.js +63 -71
  130. package/dist/services/tool-context.d.ts +0 -8
  131. package/dist/services/tool-context.js +29 -25
  132. package/dist/services/tool-icons.d.ts +4 -3
  133. package/dist/services/tool-icons.js +4 -3
  134. package/dist/services/tool-instance-id.d.ts +0 -1
  135. package/dist/services/tool-instance-id.js +0 -6
  136. package/dist/services/tool-providers/TTSToolProvider.d.ts +41 -80
  137. package/dist/services/tool-providers/TTSToolProvider.js +81 -40
  138. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  139. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
  140. package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
  141. package/dist/services/tool-providers/index.d.ts +0 -9
  142. package/dist/services/tool-providers/index.js +2 -4
  143. package/dist/services/tool-request.d.ts +27 -12
  144. package/dist/services/tool-request.js +24 -14
  145. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  146. package/dist/services/toolbar-decided-tools.js +28 -0
  147. package/dist/services/tools-config-normalizer.d.ts +37 -23
  148. package/dist/services/tools-config-normalizer.js +2 -10
  149. package/dist/services/tts/browser-provider.d.ts +38 -2
  150. package/dist/services/tts/browser-provider.js +276 -140
  151. package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
  152. package/dist/services/tts/catalog-span-alignment.js +17 -142
  153. package/dist/services/tts/flat-tree.d.ts +71 -0
  154. package/dist/services/tts/flat-tree.js +269 -0
  155. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  156. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  157. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  158. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  159. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  160. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  161. package/dist/services/tts/generated-speech/types.d.ts +0 -12
  162. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
  163. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
  164. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
  165. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  166. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  167. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  168. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  169. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  170. package/dist/services/tts/math-alignment/index.d.ts +0 -5
  171. package/dist/services/tts/math-alignment/index.js +5 -25
  172. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
  173. package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
  174. package/dist/services/tts/math-alignment/types.d.ts +4 -2
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +74 -18
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +16 -1
  180. package/dist/services/tts/math-speech.js +55 -19
  181. package/dist/services/tts/provider-options.d.ts +41 -0
  182. package/dist/services/tts/provider-options.js +1 -0
  183. package/dist/services/tts/sre-engine.d.ts +7 -0
  184. package/dist/services/tts/sre-engine.js +11 -0
  185. package/dist/services/tts/sre-locales.d.ts +14 -0
  186. package/dist/services/tts/sre-locales.js +57 -0
  187. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  188. package/dist/services/tts/ssml/spoken-text.js +29 -0
  189. package/dist/services/tts/start-failure.d.ts +11 -0
  190. package/dist/services/tts/start-failure.js +19 -0
  191. package/dist/services/tts/text-processing.d.ts +9 -1
  192. package/dist/services/tts/text-processing.js +46 -34
  193. package/dist/services/tts/text-segmentation.d.ts +7 -0
  194. package/dist/services/tts/text-segmentation.js +47 -0
  195. package/dist/services/tts-runtime-config.d.ts +44 -18
  196. package/dist/services/tts-runtime-config.js +22 -93
  197. package/dist/tools/content-capability-resolution.d.ts +7 -9
  198. package/dist/tools/content-capability-resolution.js +8 -45
  199. package/dist/tools/registration.d.ts +58 -0
  200. package/dist/tools/registration.js +54 -0
  201. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  202. package/dist/tools/tool-surface-host.d.ts +6 -10
  203. package/dist/tools/tool-surface-host.js +44 -42
  204. package/dist/tools/tool-tag-map.d.ts +5 -15
  205. package/dist/tools/tool-tag-map.js +6 -20
  206. package/dist/utils/structural-equality.d.ts +11 -0
  207. package/dist/utils/structural-equality.js +47 -0
  208. package/package.json +24 -59
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  210. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  211. package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
  212. package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
  213. package/dist/context/runtime-context-consumer.d.ts +0 -13
  214. package/dist/context/runtime-context-consumer.js +0 -51
  215. package/dist/item-loader.d.ts +0 -44
  216. package/dist/item-loader.js +0 -61
  217. package/dist/policy/core/engine-context.d.ts +0 -29
  218. package/dist/policy/core/engine-context.js +0 -27
  219. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  220. package/dist/policy/core/policy-source-tag.js +0 -28
  221. package/dist/policy/internal.d.ts +0 -25
  222. package/dist/policy/internal.js +0 -25
  223. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  224. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  225. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  226. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  227. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  228. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  229. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  230. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  231. package/dist/runtime/engine-context.d.ts +0 -39
  232. package/dist/runtime/engine-context.js +0 -31
  233. package/dist/runtime/internal.d.ts +0 -43
  234. package/dist/runtime/internal.js +0 -45
  235. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -80
  236. package/dist/runtime/section-runtime-engine-host-context.js +0 -95
  237. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  238. package/dist/runtime/stage-emit-gate.js +0 -82
  239. package/dist/services/I18nService.d.ts +0 -47
  240. package/dist/services/I18nService.js +0 -84
  241. package/dist/services/ThemeProvider.d.ts +0 -95
  242. package/dist/services/ThemeProvider.js +0 -334
  243. package/dist/services/ToolConfigResolver.d.ts +0 -161
  244. package/dist/services/ToolConfigResolver.js +0 -229
  245. package/dist/services/catalog-media.d.ts +0 -56
  246. package/dist/services/catalog-media.js +0 -190
  247. package/dist/services/createDefaultToolRegistry.d.ts +0 -53
  248. package/dist/services/createDefaultToolRegistry.js +0 -45
  249. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  250. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  251. package/dist/services/pnp-standard-features.d.ts +0 -238
  252. package/dist/services/pnp-standard-features.js +0 -338
  253. package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
  254. package/dist/services/tool-providers/CortexToolProvider.js +0 -32
  255. package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
  256. package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
  257. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
  258. package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
  259. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
  260. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
  261. package/dist/services/tts/visual-line-ranges.d.ts +0 -18
  262. package/dist/services/tts/visual-line-ranges.js +0 -226
  263. package/dist/tools/client.d.ts +0 -12
  264. package/dist/tools/client.js +0 -14
  265. package/dist/tools/index.d.ts +0 -8
  266. package/dist/tools/index.js +0 -10
  267. package/dist/tools/internal.d.ts +0 -43
  268. package/dist/tools/internal.js +0 -41
  269. package/dist/tools/response-discovery.d.ts +0 -61
  270. package/dist/tools/response-discovery.js +0 -113
  271. package/dist/tools/types.d.ts +0 -337
  272. package/dist/tools/types.js +0 -12
  273. package/dist/tools/variant-resolver.d.ts +0 -41
  274. package/dist/tools/variant-resolver.js +0 -207
@@ -1,6 +1,5 @@
1
1
  /**
2
- * PNP Policy Source for the M8 engine
3
- * (see
2
+ * PNP Policy Source: step 5 of `composeDecision` (see
4
3
  * `docs/tools-and-accomodations/architecture.md`).
5
4
  *
6
5
  * This source applies the PNP/profile precedence rules as a `(candidates, pnpPolicyInputs) →
@@ -16,12 +15,15 @@ export class PnpPolicySource {
16
15
  }
17
16
  apply(args) {
18
17
  const { ctx, result } = this.prepare(args);
19
- const { pnp, districtPolicy, itemSettings } = ctx;
18
+ const { pnp, districtPolicy, testAdmin, itemSettings } = ctx;
20
19
  const allSupports = new Set();
21
20
  pnp?.supports?.forEach((s) => allSupports.add(s));
22
21
  pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
23
22
  districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
24
23
  districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
24
+ for (const s of Object.keys(testAdmin?.toolOverrides ?? {})) {
25
+ allSupports.add(s);
26
+ }
25
27
  itemSettings?.requiredTools?.forEach((s) => allSupports.add(s));
26
28
  itemSettings?.restrictedTools?.forEach((s) => allSupports.add(s));
27
29
  for (const supportId of allSupports) {
@@ -30,20 +32,18 @@ export class PnpPolicySource {
30
32
  return result;
31
33
  }
32
34
  /**
33
- * Evaluate exactly one PNP support id through the same six-level
35
+ * Evaluate exactly one PNP support id through the same eight-level
34
36
  * precedence `apply(...)` uses.
35
37
  *
36
38
  * This exists for **policy-addressable capabilities that are not toolbar
37
39
  * tools** — a signed alternate rendered as its own region, for example.
38
40
  * `apply(...)` only evaluates support ids that appear somewhere in the
39
- * bound policy inputs, and it keys its result maps by *mapped tool id*, so
40
- * a caller asking about one feature would have to re-derive that mapping
41
- * and could collide with another support id that maps to the same tool.
42
- * Evaluating one id in isolation avoids both problems: the returned result
43
- * carries exactly one decision, and `decisions[0].action` is the verdict.
41
+ * bound policy inputs. Evaluating one id in isolation answers for any
42
+ * feature: the returned result carries exactly one decision, and
43
+ * `decisions[0].action` is the verdict.
44
44
  *
45
45
  * Reusing `resolveSupport(...)` rather than re-walking the precedence rules
46
- * is the point — a second copy of the six levels would drift.
46
+ * is the point — a second copy of the eight levels would drift.
47
47
  */
48
48
  resolveFeature(featureId, args) {
49
49
  const { ctx, result } = this.prepare(args);
@@ -55,16 +55,17 @@ export class PnpPolicySource {
55
55
  * configuration-source attribution) from the bound policy inputs.
56
56
  */
57
57
  prepare(args) {
58
- const { assessment, currentItemRef } = args;
58
+ const { assessment, item } = args;
59
59
  const pnp = assessment?.personalNeedsProfile;
60
60
  const settings = assessment?.settings;
61
- const itemSettings = currentItemRef?.settings;
61
+ const itemSettings = item?.settings;
62
62
  const result = {
63
63
  blockedToolIds: new Set(),
64
64
  mandatedToolIds: new Set(),
65
65
  perToolFlags: new Map(),
66
66
  decisions: [],
67
- unmappedSupportIds: new Set(),
67
+ unmappedSupportIds: this.unmappedIds(pnp, settings, itemSettings),
68
+ blockedOverrides: new Map(),
68
69
  sources: {},
69
70
  };
70
71
  if (settings?.districtPolicy || settings?.testAdministration) {
@@ -81,11 +82,11 @@ export class PnpPolicySource {
81
82
  config: pnp,
82
83
  };
83
84
  }
84
- if (currentItemRef && itemSettings) {
85
+ if (item) {
85
86
  result.sources.item = {
86
- id: currentItemRef.identifier,
87
- name: currentItemRef.identifier,
88
- config: itemSettings,
87
+ id: item.id,
88
+ name: item.id,
89
+ config: item.settings,
89
90
  };
90
91
  }
91
92
  const ctx = {
@@ -100,12 +101,11 @@ export class PnpPolicySource {
100
101
  resolveSupport(supportId, ctx, out) {
101
102
  // 1. District block (absolute veto)
102
103
  if (ctx.districtPolicy?.blockedTools?.includes(supportId)) {
103
- const toolId = this.mapSupportToToolId(supportId, out);
104
- out.blockedToolIds.add(toolId);
104
+ out.blockedToolIds.add(supportId);
105
105
  out.decisions.push({
106
106
  precedence: 1,
107
107
  rule: "district-block",
108
- featureId: toolId,
108
+ featureId: supportId,
109
109
  action: "block",
110
110
  sourceType: "assessment",
111
111
  reason: `District policy blocks "${supportId}" for all assessments`,
@@ -113,29 +113,32 @@ export class PnpPolicySource {
113
113
  });
114
114
  return;
115
115
  }
116
- // 2. Test administration override
117
- if (ctx.testAdmin?.toolOverrides?.[supportId] === false) {
118
- const toolId = this.mapSupportToToolId(supportId, out);
119
- out.blockedToolIds.add(toolId);
116
+ // 2. Test administration override `false`: withdraws the support for the
117
+ // session, outranking every level below.
118
+ const override = ctx.testAdmin?.toolOverrides?.[supportId];
119
+ if (override === false) {
120
+ out.blockedToolIds.add(supportId);
120
121
  out.decisions.push({
121
122
  precedence: 2,
122
123
  rule: "test-admin-override",
123
- featureId: toolId,
124
+ featureId: supportId,
124
125
  action: "block",
125
126
  sourceType: "assessment",
126
127
  reason: `Test administrator disabled "${supportId}" for this session`,
127
- value: ctx.testAdmin.toolOverrides,
128
+ value: ctx.testAdmin?.toolOverrides,
128
129
  });
129
130
  return;
130
131
  }
131
132
  // 3. Item restriction (per-item block)
132
133
  if (ctx.itemSettings?.restrictedTools?.includes(supportId)) {
133
- const toolId = this.mapSupportToToolId(supportId, out);
134
- out.blockedToolIds.add(toolId);
134
+ out.blockedToolIds.add(supportId);
135
+ if (override === true) {
136
+ out.blockedOverrides.set(supportId, "item-restriction");
137
+ }
135
138
  out.decisions.push({
136
139
  precedence: 3,
137
140
  rule: "item-restriction",
138
- featureId: toolId,
141
+ featureId: supportId,
139
142
  action: "block",
140
143
  sourceType: "item",
141
144
  reason: `Item restricts "${supportId}" (e.g., mental math question blocks calculator)`,
@@ -143,21 +146,57 @@ export class PnpPolicySource {
143
146
  });
144
147
  return;
145
148
  }
146
- // 4. Item requirement (forces enable)
149
+ // 4. Profile prohibition: the student's profile withdraws the support,
150
+ // outranking a `true` override and every requirement.
151
+ if (ctx.pnp?.prohibitedSupports?.includes(supportId)) {
152
+ out.blockedToolIds.add(supportId);
153
+ if (override === true) {
154
+ out.blockedOverrides.set(supportId, "pnp-prohibited");
155
+ }
156
+ out.decisions.push({
157
+ precedence: 4,
158
+ rule: "pnp-prohibited",
159
+ featureId: supportId,
160
+ action: "block",
161
+ sourceType: "student",
162
+ reason: `Student PNP profile prohibits "${supportId}"`,
163
+ value: ctx.pnp.prohibitedSupports,
164
+ });
165
+ return;
166
+ }
167
+ // 5. Test administration override `true`: grants the support for the
168
+ // session.
169
+ if (override === true) {
170
+ out.perToolFlags.set(supportId, {
171
+ required: false,
172
+ alwaysAvailable: true,
173
+ settings: this.resolveToolSettings(supportId, ctx),
174
+ rule: "test-admin-override",
175
+ });
176
+ out.decisions.push({
177
+ precedence: 5,
178
+ rule: "test-admin-override",
179
+ featureId: supportId,
180
+ action: "enable",
181
+ sourceType: "assessment",
182
+ reason: `Test administrator enabled "${supportId}" for this session`,
183
+ value: ctx.testAdmin?.toolOverrides,
184
+ });
185
+ return;
186
+ }
187
+ // 6. Item requirement (forces enable)
147
188
  if (ctx.itemSettings?.requiredTools?.includes(supportId)) {
148
- const toolId = this.mapSupportToToolId(supportId, out);
149
- out.mandatedToolIds.add(toolId);
150
- out.perToolFlags.set(toolId, {
189
+ out.mandatedToolIds.add(supportId);
190
+ out.perToolFlags.set(supportId, {
151
191
  required: true,
152
192
  alwaysAvailable: false,
153
193
  settings: this.resolveToolSettings(supportId, ctx),
154
194
  rule: "item-requirement",
155
- sourceType: "item",
156
195
  });
157
196
  out.decisions.push({
158
- precedence: 4,
197
+ precedence: 6,
159
198
  rule: "item-requirement",
160
- featureId: toolId,
199
+ featureId: supportId,
161
200
  action: "enable",
162
201
  sourceType: "item",
163
202
  reason: `Item requires "${supportId}" for this question`,
@@ -165,21 +204,19 @@ export class PnpPolicySource {
165
204
  });
166
205
  return;
167
206
  }
168
- // 5. District requirement
207
+ // 7. District requirement
169
208
  if (ctx.districtPolicy?.requiredTools?.includes(supportId)) {
170
- const toolId = this.mapSupportToToolId(supportId, out);
171
- out.mandatedToolIds.add(toolId);
172
- out.perToolFlags.set(toolId, {
209
+ out.mandatedToolIds.add(supportId);
210
+ out.perToolFlags.set(supportId, {
173
211
  required: true,
174
212
  alwaysAvailable: false,
175
213
  settings: this.resolveToolSettings(supportId, ctx),
176
214
  rule: "district-requirement",
177
- sourceType: "assessment",
178
215
  });
179
216
  out.decisions.push({
180
- precedence: 5,
217
+ precedence: 7,
181
218
  rule: "district-requirement",
182
- featureId: toolId,
219
+ featureId: supportId,
183
220
  action: "enable",
184
221
  sourceType: "assessment",
185
222
  reason: `District policy requires "${supportId}" for all assessments`,
@@ -187,34 +224,18 @@ export class PnpPolicySource {
187
224
  });
188
225
  return;
189
226
  }
190
- // 6. PNP prohibitions and supports (student needs)
191
- if (ctx.pnp?.prohibitedSupports?.includes(supportId)) {
192
- const toolId = this.mapSupportToToolId(supportId, out);
193
- out.blockedToolIds.add(toolId);
194
- out.decisions.push({
195
- precedence: 6,
196
- rule: "pnp-prohibited",
197
- featureId: toolId,
198
- action: "block",
199
- sourceType: "student",
200
- reason: `Student PNP profile prohibits "${supportId}"`,
201
- value: ctx.pnp.prohibitedSupports,
202
- });
203
- return;
204
- }
227
+ // 8. Profile supports (student needs)
205
228
  if (ctx.pnp?.supports?.includes(supportId)) {
206
- const toolId = this.mapSupportToToolId(supportId, out);
207
- out.perToolFlags.set(toolId, {
229
+ out.perToolFlags.set(supportId, {
208
230
  required: false,
209
231
  alwaysAvailable: true,
210
232
  settings: this.resolveToolSettings(supportId, ctx),
211
233
  rule: "pnp-support",
212
- sourceType: "student",
213
234
  });
214
235
  out.decisions.push({
215
- precedence: 6,
236
+ precedence: 8,
216
237
  rule: "pnp-support",
217
- featureId: toolId,
238
+ featureId: supportId,
218
239
  action: "enable",
219
240
  sourceType: "student",
220
241
  reason: `Student PNP profile requests "${supportId}"`,
@@ -222,16 +243,12 @@ export class PnpPolicySource {
222
243
  });
223
244
  return;
224
245
  }
225
- // 6 (skip): support-id mentioned somewhere but no rule fired.
226
- // Use the mapped tool id as `featureId` so the trail is keyed
227
- // the same way as every other branch — PR 5's debugger surfaces
228
- // index provenance trails by tool id and would otherwise show
229
- // orphaned support-id entries here (M8 PR 1 R3 S1).
230
- const toolId = this.mapSupportToToolId(supportId, out);
246
+ // 8 (skip): no rule fired. The trail is keyed by tool id, as in every
247
+ // other branch.
231
248
  out.decisions.push({
232
- precedence: 6,
249
+ precedence: 8,
233
250
  rule: "pnp-support",
234
- featureId: toolId,
251
+ featureId: supportId,
235
252
  action: "skip",
236
253
  sourceType: "system",
237
254
  reason: `Feature "${supportId}" not configured at any level`,
@@ -239,57 +256,39 @@ export class PnpPolicySource {
239
256
  });
240
257
  }
241
258
  /**
242
- * Map a QTI / PNP support id (e.g. `"calculator-basic"`) to the
243
- * registered tool id that owns it.
244
- *
245
- * Resolution rules:
246
- *
247
- * 1. **Multiple tools register the same support id:** the
248
- * *first-registered* tool wins. `ToolRegistry.getToolsByPNPSupport`
249
- * returns a `Set<string>` whose iteration order matches the
250
- * `register(...)` insertion order, so the result is
251
- * deterministic across runs but order-sensitive at registration
252
- * time. This is an unusual configuration — the typical case is
253
- * one tool per support id — but it is reachable when an
254
- * integrator overrides a default tool with a replacement that
255
- * claims the same `pnpSupportIds`. In that case the integrator
256
- * should `unregister(...)` the default before registering the
257
- * replacement.
258
- *
259
- * 2. **No tool registers the support id:** the support id is
260
- * returned verbatim as the `featureId` for provenance trails
261
- * and `ToolPolicyEntry.toolId`. This lets hosts use raw QTI
262
- * strings for tools the registry does not (yet) carry without
263
- * losing them in policy evaluation.
264
- *
265
- * `tests/policy/PnpPolicySource.test.ts` locks this mapping behavior.
259
+ * The named ids no tool is registered under, with the rules naming each in
260
+ * precedence order. A support id is the id of the tool it grants, so an id
261
+ * missing from the registry matches nothing placed.
266
262
  */
267
- mapSupportToToolId(supportId, out) {
268
- const toolIds = this.toolRegistry.getToolsByPNPSupport(supportId);
269
- if (toolIds.size === 0) {
270
- // An empty registry means there is nothing to check the id against, not
271
- // that the id is wrong — reporting it there would fire on every support
272
- // id for a host that supplies no registry, which
273
- // `tool-config-validation` already warns about once.
274
- if (this.toolRegistry.getAllTools().length > 0) {
275
- out.unmappedSupportIds.add(supportId);
263
+ unmappedIds(pnp, settings, itemSettings) {
264
+ const unmapped = new Map();
265
+ if (this.toolRegistry.getAllTools().length === 0)
266
+ return unmapped;
267
+ const named = [
268
+ ["district-block", settings?.districtPolicy?.blockedTools],
269
+ [
270
+ "test-admin-override",
271
+ Object.keys(settings?.testAdministration?.toolOverrides ?? {}),
272
+ ],
273
+ ["item-restriction", itemSettings?.restrictedTools],
274
+ ["pnp-prohibited", pnp?.prohibitedSupports],
275
+ ["item-requirement", itemSettings?.requiredTools],
276
+ ["district-requirement", settings?.districtPolicy?.requiredTools],
277
+ ["pnp-support", pnp?.supports],
278
+ ];
279
+ for (const [rule, ids] of named) {
280
+ for (const id of ids ?? []) {
281
+ if (this.toolRegistry.has(id))
282
+ continue;
283
+ const rules = unmapped.get(id) ?? [];
284
+ if (!rules.includes(rule))
285
+ rules.push(rule);
286
+ unmapped.set(id, rules);
276
287
  }
277
- return supportId;
278
288
  }
279
- return Array.from(toolIds)[0];
289
+ return unmapped;
280
290
  }
281
- /**
282
- * Resolve tool-specific settings, item-level taking precedence over
283
- * assessment-level.
284
- *
285
- * Uses `??` (nullish-coalescing). Tool config values are typed `unknown` and may legitimately
286
- * be `false` / `0` / `""` (e.g. a tool whose canonical "off" config
287
- * is the literal `false`). `||` would treat those as missing and
288
- * silently fall through to the assessment-level config, masking the
289
- * authored item-level intent. `??` only falls through on
290
- * `null`/`undefined`, which matches the semantic of "value not
291
- * provided."
292
- */
291
+ /** Tool parameters: the item's entry, else the assessment's. */
293
292
  resolveToolSettings(supportId, ctx) {
294
293
  return (ctx.itemSettings?.toolParameters?.[supportId] ??
295
294
  ctx.toolConfigs?.[supportId]);
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Section controller binding — the toolkit's side of a section runtime.
3
+ *
4
+ * `PieAssessmentToolkit` holds one per element. It resolves the section's
5
+ * controller from the coordinator (`initialize`), keeps the registered item and
6
+ * passage shells (`RuntimeRegistry`) and which of them have loaded, replays
7
+ * both into each controller it resolves, and forwards shell events, session
8
+ * updates and host commands to that controller.
9
+ *
10
+ * The registry outlives a section switch: the shells of the section being left
11
+ * stay registered until they unmount. One membership rule routes every shell
12
+ * event, in replay or live: an event goes to the controller of the section
13
+ * whose composition renders the shell, which is the current controller or a
14
+ * cached controller of a section this binding bound earlier, and is dropped
15
+ * with a warning when no bound section renders it; see `controllerFor`. A
16
+ * cached controller re-bound on a revisit first forgets the loads of shells
17
+ * that are no longer registered, so its `section-loading-complete` waits for
18
+ * the new shells.
19
+ *
20
+ * The section's stage chain is a separate object: the section player's layout
21
+ * kernel drives a `SectionRuntimeEngine` from the readiness signals it derives.
22
+ * The two share no state.
23
+ */
24
+ import type { ToolkitCoordinator } from "../services/ToolkitCoordinator.js";
25
+ import type { SectionControllerEvent, SectionControllerHandle, SectionControllerSessionState } from "../services/section-controller-types.js";
26
+ import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
27
+ import type { RuntimeRegistrationDetail } from "./registration-events.js";
28
+ import { RuntimeRegistry } from "./RuntimeRegistry.js";
29
+ /**
30
+ * Structural view of the resolved section controller, widened with the
31
+ * extra methods the toolkit's runtime CE calls directly. Optional so
32
+ * stub controllers used in tests do not need every entry point.
33
+ */
34
+ interface RuntimeController extends SectionControllerHandle {
35
+ getCompositionModel?: () => unknown;
36
+ getCanonicalItemId?: (itemId: string) => string;
37
+ handleContentLoaded?: (args: {
38
+ itemId: string;
39
+ canonicalItemId?: string;
40
+ contentKind?: string;
41
+ detail?: unknown;
42
+ timestamp?: number;
43
+ }) => void;
44
+ handleContentRegistered?: (args: {
45
+ itemId: string;
46
+ canonicalItemId?: string;
47
+ contentKind?: string;
48
+ }) => void;
49
+ handleContentUnregistered?: (args: {
50
+ itemId: string;
51
+ canonicalItemId?: string;
52
+ contentKind?: string;
53
+ }) => void;
54
+ handleItemPlayerError?: (args: {
55
+ itemId: string;
56
+ canonicalItemId?: string;
57
+ contentKind?: string;
58
+ error: unknown;
59
+ timestamp?: number;
60
+ }) => void;
61
+ reportSectionError?: (args: {
62
+ source: "item-player" | "section-runtime" | "toolkit" | "controller";
63
+ error: unknown;
64
+ itemId?: string;
65
+ canonicalItemId?: string;
66
+ contentKind?: string;
67
+ timestamp?: number;
68
+ }) => void;
69
+ updateItemSession?: (itemId: string, sessionDetail: unknown) => {
70
+ eventDetail?: unknown;
71
+ } | null;
72
+ subscribe?: (listener: (event: SectionControllerEvent) => void) => () => void;
73
+ navigateToItem?: (index: number) => unknown;
74
+ attachMediaTimeSource?: (source: MediaTimeSource, options?: {
75
+ origin?: "native-adapter" | "host";
76
+ renderableId?: string;
77
+ }) => void;
78
+ detachMediaTimeSource?: (options?: {
79
+ origin?: "native-adapter" | "host";
80
+ }) => void;
81
+ pauseMediaForCompetingAudio?: () => boolean;
82
+ }
83
+ /** What a media-time-source registration reaching the binding has to say. */
84
+ export interface SectionRuntimeMediaTimeSourceAction {
85
+ renderableId: string;
86
+ action: "attach" | "detach";
87
+ source?: MediaTimeSource;
88
+ origin?: "native-adapter" | "host";
89
+ }
90
+ /** What a formative action reaching the binding has to say. */
91
+ export interface SectionRuntimeFormativeAction {
92
+ itemId: string;
93
+ canonicalItemId?: string;
94
+ action: "check" | "retry";
95
+ outcomes?: unknown[];
96
+ }
97
+ /**
98
+ * Initialize args used by the toolkit CE to resolve the coordinator-backed
99
+ * section controller.
100
+ */
101
+ export interface SectionControllerBindingInitArgs {
102
+ coordinator: ToolkitCoordinator;
103
+ section: unknown;
104
+ sectionId: string;
105
+ assessmentId: string;
106
+ view: string;
107
+ attemptId?: string;
108
+ /**
109
+ * A host-supplied session for this section, applied by the coordinator in
110
+ * place of `hydrate()` when it creates the controller. Absent, creation
111
+ * hydrates from the persistence strategy as before.
112
+ */
113
+ initialSession?: SectionControllerSessionState | null;
114
+ createDefaultController: () => Promise<RuntimeController> | RuntimeController;
115
+ onCompositionChanged?: (composition: unknown) => void;
116
+ /**
117
+ * The controller event that caused a republish, for the few events the toolkit
118
+ * has to act on rather than merely propagate — a timed-media policy that cannot
119
+ * be enforced becomes a framework warning, and malformed cue data becomes a
120
+ * framework error. Every other event reaches hosts through the composition
121
+ * republish and the coordinator's own subscriptions.
122
+ */
123
+ onControllerEvent?: (event: SectionControllerEvent) => void;
124
+ }
125
+ export declare class SectionControllerBinding {
126
+ private readonly registry;
127
+ private controller;
128
+ private coordinator;
129
+ private sectionId;
130
+ private attemptId;
131
+ private unsubscribeController;
132
+ private activeInitToken;
133
+ /**
134
+ * Ids of the renderables in the current controller's composition model, read
135
+ * when `initialize` resolves it. `null` while no controller is resolved, and
136
+ * for a controller that publishes no `renderables`: its section cannot be
137
+ * told apart, so every shell reaches it.
138
+ */
139
+ private sectionRenderableIds;
140
+ /**
141
+ * The renderable ids of every section this binding has bound, keyed by
142
+ * section and attempt, least recently bound first. `controllerFor` reads it
143
+ * to find the section that owns a shell outside the current one.
144
+ */
145
+ private readonly boundSections;
146
+ /**
147
+ * Tracks which `(canonicalItemId, contentKind)` pairs have already
148
+ * fired `handleContentLoaded` on this binding, so a section handoff can
149
+ * replay the persistent shells' loaded state into the new controller; see
150
+ * `replayRegisteredShellsIntoController`.
151
+ *
152
+ * Records loads of every registered shell, in or out of the current
153
+ * section, so a shell registered ahead of its section's controller is
154
+ * replayed as loaded. A key is dropped when its last shell unregisters,
155
+ * and on `dispose`.
156
+ */
157
+ private readonly loadedRenderableKeys;
158
+ initialize(args: SectionControllerBindingInitArgs): Promise<void>;
159
+ /**
160
+ * Re-feed the binding's `RuntimeRegistry` and loaded-set into the
161
+ * resolved controller, limited to the shells of its section (see
162
+ * `isSectionRenderable`). Runs on every `initialize`.
163
+ *
164
+ * Two-pass order — register every shell in document order, then
165
+ * issue `handleContentLoaded` for each shell whose load already
166
+ * fired on this binding. The two-pass shape prevents
167
+ * `evaluateSectionLoadingState` from flapping
168
+ * `section-loading-complete` `false→true→false→…` while replay is
169
+ * mid-walk: any subscriber wired before the binding's own
170
+ * `controller.subscribe` (no such caller today, but cheap defense
171
+ * for future wiring) sees one clean `false→true` transition.
172
+ *
173
+ * Idempotent on the controller side: `handleContentRegistered` and
174
+ * `handleContentLoaded` return early on duplicates, so re-feeding shells
175
+ * the controller already tracks emits nothing.
176
+ */
177
+ private replayRegisteredShellsIntoController;
178
+ /**
179
+ * Stable key for the binding's `loadedRenderableKeys` set. Mirrors
180
+ * the `(canonicalItemId, contentKind)` shape `SectionController`
181
+ * uses internally (see `getRenderableKey`); we only need
182
+ * consistent add/check semantics on this side, not byte-identical
183
+ * normalization with the controller.
184
+ */
185
+ private getLoadedKey;
186
+ /**
187
+ * Whether a shell renders one of the current section's renderables: its
188
+ * runtime or canonical id is among the composition model's renderable ids.
189
+ * An item in two sections belongs to both, so its shell carries over a
190
+ * switch the way a persistent passage shell does.
191
+ */
192
+ private isSectionRenderable;
193
+ private getSectionKey;
194
+ /**
195
+ * The controller a shell event belongs to: the current controller when the
196
+ * shell renders one of its section's renderables, otherwise the cached
197
+ * controller of the most recently bound section that renders it — the
198
+ * section being left, for an outgoing shell's teardown. `null` when no bound
199
+ * section renders the shell or its controller is gone.
200
+ */
201
+ private controllerFor;
202
+ /** `controllerFor`, warning when the event has nowhere to go. */
203
+ private requireControllerFor;
204
+ /**
205
+ * Forget, on a cached controller about to be re-bound, every load whose
206
+ * shell is no longer registered. A shell that unmounted while its section
207
+ * was not current told that section's controller through `controllerFor`;
208
+ * this covers one whose unregister never arrived.
209
+ */
210
+ private forgetUnregisteredLoads;
211
+ register(detail: RuntimeRegistrationDetail): boolean;
212
+ unregister(element: HTMLElement): boolean;
213
+ getCompositionModel(): unknown;
214
+ getCanonicalItemId(itemId: string): string;
215
+ handleContentRegistered(detail: RuntimeRegistrationDetail): void;
216
+ /**
217
+ * Called after `unregister` removed the shell from the registry. While
218
+ * another registered shell renders the same renderable — the next
219
+ * section's shell for an item both sections hold — the renderable stays
220
+ * registered and loaded.
221
+ */
222
+ handleContentUnregistered(detail: RuntimeRegistrationDetail): void;
223
+ handleContentLoaded(args: {
224
+ itemId: string;
225
+ canonicalItemId?: string;
226
+ contentKind?: string;
227
+ detail?: unknown;
228
+ timestamp?: number;
229
+ }): void;
230
+ handleItemPlayerError(args: {
231
+ itemId: string;
232
+ canonicalItemId?: string;
233
+ contentKind?: string;
234
+ error: unknown;
235
+ timestamp?: number;
236
+ }): void;
237
+ reportSectionError(args: {
238
+ source: "item-player" | "section-runtime" | "toolkit" | "controller";
239
+ error: unknown;
240
+ itemId?: string;
241
+ canonicalItemId?: string;
242
+ contentKind?: string;
243
+ timestamp?: number;
244
+ }): void;
245
+ updateItemSession(itemId: string, session: unknown): unknown;
246
+ navigateToItem(index: number): unknown;
247
+ /**
248
+ * Route a learner's formative action to the controller.
249
+ *
250
+ * Canonicalized here for the same reason `updateItemSession` is: the runtime
251
+ * id a card dispatches with is not necessarily the identifier the controller
252
+ * keys state by. Both ids are kept, since the composition's renderables are
253
+ * keyed by the runtime one.
254
+ */
255
+ handleFormativeAction(action: SectionRuntimeFormativeAction): void;
256
+ /**
257
+ * Bind or release the section's Media Time Source.
258
+ *
259
+ * A pass-through, like `handleFormativeAction`: the binding routes, the
260
+ * controller decides. `renderableId` travels with the attach because whether the
261
+ * registering renderable is the one `stimulusRef` names is the controller's call
262
+ * — it validated `stimulusRef` in the first place.
263
+ */
264
+ handleMediaTimeSource(action: SectionRuntimeMediaTimeSourceAction): void;
265
+ /**
266
+ * Silence media audio because something else is about to speak.
267
+ *
268
+ * A pass-through like `handleMediaTimeSource`, and for the same reason: which
269
+ * source is authoritative and whether it reports `canPause` are the controller's
270
+ * to know. Returns whether media audio is now silent, or `true` where there is
271
+ * no timed-media controller to ask — nothing is playing.
272
+ */
273
+ requestMediaPauseForCompetingAudio(): boolean;
274
+ persist(): Promise<void>;
275
+ hydrate(): Promise<void>;
276
+ getRegistry(): RuntimeRegistry;
277
+ dispose(): Promise<void>;
278
+ }
279
+ export {};