@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
package/README.md CHANGED
@@ -4,6 +4,30 @@
4
4
 
5
5
  This is not an opinionated framework or monolithic "player" - it's a toolkit that solves specific problems through centralized service management.
6
6
 
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @pie-players/pie-assessment-toolkit @pie-players/pie-default-tool-loaders
11
+ ```
12
+
13
+ The examples build their tool registry with `createPackagedToolRegistry()` from
14
+ `@pie-players/pie-default-tool-loaders`, so a host that imports it declares it
15
+ in its own dependencies. The section player depends on it too, but strict
16
+ installers such as pnpm expose only the packages a host declares.
17
+
18
+ Server-backed TTS (`backend: "server"`) loads
19
+ `@pie-players/tts-client-server`, a dependency of
20
+ `@pie-players/pie-default-tool-loaders` whose TTS registration imports it on
21
+ first use. If it fails to load, TTS initialization reports a `provider-init`
22
+ framework error and falls back to browser speech.
23
+
24
+ A `ToolkitCoordinator` registers tool providers only from its `toolRegistry`.
25
+ Built without one, it adopts the registry of the toolkit it is bound to, such as
26
+ the section player's, which then validates its config and registers its
27
+ providers. A registry passed at construction is never replaced. Bound to a
28
+ toolkit that has none, it registers no providers, skips tool-id and placement
29
+ validation, and warns once (`tools.registryUnavailable`).
30
+
7
31
  ## What's New: ToolkitCoordinator
8
32
 
9
33
  ✨ **Centralized Service Management**: The new `ToolkitCoordinator` provides a single entry point for all toolkit services, simplifying initialization and configuration.
@@ -28,16 +52,26 @@ player.toolCoordinator = toolCoordinator;
28
52
 
29
53
  **After** (coordinator orchestrates):
30
54
  ```typescript
55
+ import {
56
+ createPackagedToolRegistry,
57
+ DEFAULT_TOOL_MODULE_LOADERS,
58
+ } from '@pie-players/pie-default-tool-loaders';
59
+
31
60
  // Create one coordinator with configuration
61
+ const toolRegistry = createPackagedToolRegistry({
62
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
63
+ });
32
64
  const toolkitCoordinator = new ToolkitCoordinator({
33
65
  assessmentId: 'my-assessment',
66
+ toolRegistry,
34
67
  tools: {
35
68
  providers: {
36
69
  textToSpeech: { enabled: true, backend: 'browser' },
70
+ // Desmos needs an application key; see "Simple Default" below.
37
71
  calculator: { enabled: true }
38
72
  },
39
73
  placement: {
40
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
74
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
41
75
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
42
76
  passage: ['textToSpeech']
43
77
  }
@@ -86,6 +120,7 @@ provider path is the item-player loader config:
86
120
 
87
121
  - `pie-toolkit-runtime-owned`
88
122
  - `pie-toolkit-runtime-inherited`
123
+ - `pie-toolkit-runtime-ready`
89
124
  - `pie-toolkit-ready`
90
125
  - `pie-toolkit-section-ready`
91
126
  - `pie-toolkit-framework-error`
@@ -109,20 +144,20 @@ See the [ToolkitCoordinator section in the architecture overview](../../docs/arc
109
144
  1. **Centralized Coordination**: ToolkitCoordinator orchestrates all services
110
145
  2. **Composable Services**: Import only what you need (or use coordinator for convenience)
111
146
  3. **No Framework Lock-in**: Works with any JavaScript framework
112
- 4. **Product Control**: Products control navigation, persistence, layout, backend
147
+ 4. **Product Control**: Products control navigation, layout and backend. Session persistence defaults to `localStorage` at the section and assessment layers, stored per attempt id and inactive without one, and a product replaces either through its persistence hook (`createSectionSessionPersistence` on the coordinator, `createAssessmentSessionPersistence` on the assessment player)
113
148
  5. **Standard Contracts**: Well-defined event types for component communication
114
149
  6. **Element-Level Granularity**: Tool state tracked per PIE element, not per item
115
150
  7. **State Separation**: Tool state (ephemeral) separate from PIE session data (persistent)
116
151
 
117
- ## Configuration tiers: easy attribute + sophisticated `runtime`
152
+ ## Configuration: attributes and a constructed coordinator
118
153
 
119
- This package and `@pie-players/pie-section-player` follow a deliberate
120
- two-tier configuration model. The same knob can usually be set in either
121
- tier; the choice is about ergonomics, not capability.
154
+ This package and `@pie-players/pie-section-player` give each input one entry
155
+ point (see [One tier per input](#one-tier-per-input)). A host that needs more
156
+ than the element inputs builds the coordinator itself.
122
157
 
123
- ### When to use each tier
158
+ ### Element inputs and a constructed coordinator
124
159
 
125
- - **Easy tier — top-level CE attributes / properties.** Use these for the
160
+ - **Element attributes / properties.** Use these for the
126
161
  common cases that are static for the lifetime of the player or that hosts
127
162
  want to set declaratively in HTML / templating frameworks. Example:
128
163
 
@@ -134,15 +169,24 @@ tier; the choice is about ergonomics, not capability.
134
169
  ></pie-assessment-toolkit>
135
170
  ```
136
171
 
137
- - **Sophisticated tier — passing a constructed `ToolkitCoordinator` (or a
172
+ - **A constructed `ToolkitCoordinator` (or a
138
173
  `runtime` object on consumer CEs).** Use this for advanced cases: composed
139
- configuration, dynamic overrides, runtime mutation, fields without a
140
- tier-1 attribute, or anything that benefits from being a single typed
174
+ configuration, dynamic overrides, runtime mutation, fields without an
175
+ attribute, or anything that benefits from being a single typed
141
176
  object passed by reference. Example:
142
177
 
143
178
  ```ts
179
+ import {
180
+ createPackagedToolRegistry,
181
+ DEFAULT_TOOL_MODULE_LOADERS,
182
+ } from "@pie-players/pie-default-tool-loaders";
183
+
184
+ const toolRegistry = createPackagedToolRegistry({
185
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
186
+ });
144
187
  const coordinator = new ToolkitCoordinator({
145
188
  assessmentId: "my-assessment",
189
+ toolRegistry,
146
190
  toolConfigStrictness: "warn",
147
191
  tools: {
148
192
  providers: { calculator: { enabled: true } },
@@ -158,38 +202,32 @@ Top-level attributes use kebab-case (`assessment-id`,
158
202
  `tool-config-strictness`). Section-player runtime configuration is grouped
159
203
  under the `runtime` object instead of duplicated as top-level props.
160
204
 
161
- ### Precedence rule
205
+ ### One tier per input
162
206
 
163
- The configuration object owns runtime fields. Section-player layout attributes
164
- cover identity, layout, diagnostics, and callback/event convenience:
207
+ Each input has one entry point. On the section-player layout elements,
208
+ `runtime` carries `assessmentId`, the player, tool, accessibility,
209
+ coordinator, env and isolation fields, runtime factories and the `on*`
210
+ callbacks; attributes carry section identity, layout controls, `nds-icons`,
211
+ `locale` and `tool-config-strictness`. Unset inputs take their documented
212
+ defaults.
165
213
 
166
- 1. Use `runtime.<key>` for player, tool, accessibility, coordinator, env,
167
- isolation, and runtime factories.
168
- 2. Use top-level layout attributes for section identity and layout controls.
169
- 3. Use documented defaults when neither is provided.
214
+ `<pie-assessment-toolkit>` takes the same fields as its own properties,
215
+ with `assessment-id`, `nds-icons`, `locale` and `tool-config-strictness` as
216
+ attributes, and reads `pnpEnforcement` from `tools.pnpEnforcement`.
170
217
 
171
- ### Canonical tier-1 attribute set
172
-
173
- The tier-1 attribute set is the same shape across
174
- `pie-assessment-toolkit`, `pie-section-player-base`, and the
175
- `pie-section-player-*` layout elements (locked in M5). Every tier-1
176
- surface obeys the strict mirror rule:
177
-
178
- ```
179
- kebab-attribute ↔ camelCaseProp ↔ runtime.<sameCamelCaseKey>
180
- ```
218
+ ### Canonical attribute set
181
219
 
182
- Common members include:
183
-
184
- - Identity: `assessment-id`, `section-id`, `attempt-id`
220
+ - Identity: `assessment-id` on the toolkit, `section-id`, `attempt-id`
185
221
  - Runtime config on section-player CEs: `runtime`
186
222
  - Toolkit-only object properties: `tools`, `tool-registry`, `coordinator`,
187
223
  `accessibility`
224
+ - Interface: `nds-icons`, `locale`
188
225
  - Diagnostics: `tool-config-strictness`, `debug`. Framework-error
189
- delivery is via the canonical `onFrameworkError` callback prop and the
226
+ delivery is via the `onFrameworkError` callback (a toolkit property,
227
+ `runtime.onFrameworkError` on a section player) and the
190
228
  `framework-error` DOM event dispatched on the layout CE host.
191
229
 
192
- Documented exceptions to the mirror rule:
230
+ Inputs outside the runtime config:
193
231
 
194
232
  - Identity (`section-id`, `attempt-id`, `section`): per-attempt host
195
233
  state, not configuration.
@@ -201,17 +239,15 @@ Documented exceptions to the mirror rule:
201
239
  `runtime.tools.placement.{item,passage}` directly.
202
240
  - Runtime-only keys on the section-player layout CEs
203
241
  (`createSectionController`, `isolation`): accepted only via
204
- `runtime.<key>`. `<pie-assessment-toolkit>`
205
- itself keeps `createSectionController` and `isolation` as JS-only
206
- props (no kebab-attribute surface): section-player layouts forward
207
- `runtime.isolation` and `runtime.createSectionController` to the
208
- wrapped toolkit via property bindings; standalone hosts that need
209
- to override coordinator inheritance should pass an explicit
210
- `coordinator={...}` instead.
242
+ `runtime.<key>`. Section-player layouts forward both to the wrapped
243
+ toolkit as properties. `<pie-assessment-toolkit>` keeps
244
+ `createSectionController` JS-only and takes `isolation` as an
245
+ attribute or a property. A `coordinator` passed to it wins over an
246
+ outer toolkit's.
211
247
 
212
- ### When to add a tier-1 attribute
248
+ ### When to add an attribute
213
249
 
214
- Add a tier-1 attribute only if all of the following hold:
250
+ Add an attribute only if all of the following hold:
215
251
 
216
252
  - It is a common case that hosts set without composing a `ToolkitCoordinator`
217
253
  / `runtime` object.
@@ -229,17 +265,26 @@ Otherwise expose it through the configuration object only.
229
265
 
230
266
  ```typescript
231
267
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
268
+ import {
269
+ createPackagedToolRegistry,
270
+ DEFAULT_TOOL_MODULE_LOADERS,
271
+ } from '@pie-players/pie-default-tool-loaders';
232
272
 
233
273
  // Create coordinator with configuration
274
+ const toolRegistry = createPackagedToolRegistry({
275
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
276
+ });
234
277
  const coordinator = new ToolkitCoordinator({
235
278
  assessmentId: 'demo-assessment',
279
+ toolRegistry,
236
280
  tools: {
237
281
  providers: {
238
282
  textToSpeech: { enabled: true, backend: 'browser' },
283
+ // Desmos needs an application key; see "Simple Default" below.
239
284
  calculator: { enabled: true }
240
285
  },
241
286
  placement: {
242
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
287
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
243
288
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
244
289
  passage: ['textToSpeech']
245
290
  }
@@ -286,6 +331,7 @@ Behavior pins (PIE-512 Phase D):
286
331
 
287
332
  - Subscribe **after** the first `getOrCreateSectionController(...)` resolves; calling subscribe before any cohort exists throws.
288
333
  - On every cohort transition (navigation, fresh `getOrCreateSectionController` for a new section), the listener is automatically migrated to the new controller and receives a snapshot replay (`content-loaded` × N then `section-loading-complete`) in the same order a fresh subscriber would have seen.
334
+ - A section's subscriptions receive `content-loaded` only for that section's renderables, live or replayed; the previous section's loads stay with it. A renderable in both sections, such as a passage that stays mounted across the switch, counts as loaded in each.
289
335
  - Subscribing the **same listener function** twice replaces the first subscription (filter args from the second call win).
290
336
  - A listener that throws is caught and `console.warn`-logged; the throw does not interrupt fan-out to other listeners.
291
337
 
@@ -319,7 +365,7 @@ What this means in practice for typed integrations:
319
365
 
320
366
  - **TypeScript breaking change.** `SectionEventSubscriptionArgs`, `SectionItemEventSubscriptionArgs`, and `SectionScopedEventSubscriptionArgs` no longer declare `sectionId?` / `attemptId?` properties. Any host that imports these arg types directly and passes those keys will fail to compile after upgrade. **Action required.**
321
367
  - **Runtime is tolerant.** The runtime silently ignores extra unknown properties, so an untyped or lightly-typed call site that still passes `sectionId` / `attemptId` continues to work without source changes. The args have **no effect** at runtime — the subscription always follows the active cohort.
322
- - **New precondition.** `subscribe*` now throws if no active section cohort exists. Subscribe **after** the first `getOrCreateSectionController(...)` resolves. Subscribing on `toolkit-ready` alone is no longer sufficient — though in practice the section player emits `toolkit-ready` *after* its first `getOrCreateSectionController(...)` resolves, so a `toolkit-ready` anchor is safe in section-player hosts.
368
+ - **New precondition.** `subscribe*` throws until the first `getOrCreateSectionController(...)` call; a listener added while a section is starting binds when that section becomes active. Subscribe **after** the first `getOrCreateSectionController(...)` resolves. Subscribing on `toolkit-ready` alone is no longer sufficient — though in practice the section player emits `toolkit-ready` *after* its first `getOrCreateSectionController(...)` resolves, so a `toolkit-ready` anchor is safe in section-player hosts.
323
369
  - **Cohort migration is automatic.** If your wrapper previously re-subscribed on every navigation to keep listeners alive across sections, that wiring is no longer needed (and should be removed). A single subscribe call after the first controller-resolve is now enough — the listener migrates automatically and is replayed the new cohort's snapshot on every transition.
324
370
  - **Watch for double-replay if you re-subscribe on every `toolkit-ready`.** Hosts that detached and re-subscribed on every `toolkit-ready` event (the correct pre-Phase D pattern, since each subscription was pinned to a `sectionId`) will now observe **two snapshot replays per navigation**: one delivered automatically when Phase D migrates the existing listener to the new active cohort, and a second when the manual re-subscribe attaches a fresh listener that replays again. Listener handlers that are not strictly idempotent will fire twice — analytics `pageAction`s, non-Set counters, side-effecting hydration. The fix is a one-line guard (`if (this.controllerUnsubscribe) return;`) so the subscribe runs only on the first `toolkit-ready`.
325
371
  - **For intentionally-pinned subscriptions to inactive sections** (e.g. a host UI that wants to keep watching section A while the user views section B), the helper API does not support that pattern by design. Use `coordinator.getSectionController({ sectionId, attemptId })` and subscribe directly on the controller handle (`controller.subscribe?.(...)`) — that binding is pinned to one controller instance and does not migrate.
@@ -387,18 +433,136 @@ const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
387
433
  await ttsService.initialize(new BrowserTTSProvider());
388
434
  ttsService.setCatalogResolver(catalogResolver);
389
435
 
390
- // Pass services individually
391
- player.ttsService = ttsService;
392
- player.toolCoordinator = toolCoordinator;
393
- // ...
436
+ // Use them from host-built UI
437
+ ```
438
+
439
+ No player element takes services one by one. The section and assessment
440
+ players reach services only through a coordinator, `runtime.coordinator` on a
441
+ section player and `coordinator` on an assessment player; manually created
442
+ services serve host code that drives them directly.
443
+
444
+ ### Without a Section Player
445
+
446
+ `<pie-assessment-toolkit>` needs no section. Bind none and it provides tools,
447
+ policy and services to the item toolbars and item players inside it, which is
448
+ how the toolkit accompanies a plain item player. `<pie-item-scope>` holds the
449
+ item for its tools, as it does in a section player's card. The tree this
450
+ builds:
451
+
452
+ ```html
453
+ <pie-assessment-toolkit>
454
+ <pie-item-scope item-id="q1">
455
+ <pie-item-toolbar></pie-item-toolbar>
456
+ <div data-region="content">
457
+ <pie-item-player></pie-item-player>
458
+ </div>
459
+ </pie-item-scope>
460
+ </pie-assessment-toolkit>
461
+ ```
462
+
463
+ The host sets the toolkit's properties before the tree enters the document. A
464
+ scope registers as soon as it mounts, and that first registration starts the
465
+ coordinator from the toolkit's inputs at that moment; see the binding rules
466
+ below.
467
+
468
+ ```typescript
469
+ import '@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element';
470
+ import '@pie-players/pie-assessment-toolkit/components/item-scope-element';
471
+ import '@pie-players/pie-assessment-toolkit/components/item-toolbar-element';
472
+ import '@pie-players/pie-item-player';
473
+ import {
474
+ createPackagedToolRegistry,
475
+ DEFAULT_TOOL_MODULE_LOADERS,
476
+ } from '@pie-players/pie-default-tool-loaders';
477
+
478
+ // With no section player, the registry's loaders are what define the tool elements.
479
+ const toolRegistry = createPackagedToolRegistry({
480
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
481
+ });
482
+
483
+ const toolkit = document.createElement('pie-assessment-toolkit');
484
+ toolkit.tools = {
485
+ pnpEnforcement: 'on',
486
+ placement: { item: ['textToSpeech', 'calculator'] },
487
+ };
488
+ toolkit.toolRegistry = toolRegistry;
489
+ toolkit.toolContextResolvers = toolContextResolvers;
490
+ toolkit.assessment = { id: 'a1', personalNeedsProfile: { supports: ['calculator'] } };
491
+
492
+ const scope = document.createElement('pie-item-scope');
493
+ scope.setAttribute('item-id', 'q1');
494
+ scope.item = item;
495
+ scope.innerHTML = `
496
+ <pie-item-toolbar></pie-item-toolbar>
497
+ <div data-region="content"><pie-item-player></pie-item-player></div>
498
+ `;
499
+
500
+ toolkit.append(scope);
501
+ container.append(toolkit);
394
502
  ```
395
503
 
504
+ The toolbars and tools inside the scope take the item and its id from it, and
505
+ act on the scope, or on the element its `scopeElement` property names;
506
+ `textToSpeech` reads the scope's `[data-region="content"]` first. A toolbar's
507
+ own `item`, `item-id` and `scopeElement` still override the scope, and its
508
+ `toolRegistry` the toolkit's. The scope
509
+ registers the item with the toolkit once it finds one above it, so it may mount
510
+ first, and the toolkit files the item's accessibility catalogs, which read-aloud
511
+ speaks in place of the markup they name. Item player events pass through the
512
+ scope unchanged, and the session stays the item player's. A registry built
513
+ without `toolModuleLoaders` renders toolbar buttons whose tool elements never
514
+ load.
515
+
516
+ A profile change is a new `assessment` value; the toolbars re-derive on the
517
+ policy change it emits. The toolkit announces `runtime-ready`, with
518
+ `{ runtimeId, coordinator, ownership }`, once per coordinator, with or without a
519
+ section. Without one, the coordinator starts at the first item that registers,
520
+ and a host reads its readiness from `coordinator.waitUntilReady()` or
521
+ `isReady()`; `toolkit-ready` and `section-ready` wait for a bound section. The
522
+ toolkit emits no stage events: `pie-stage-change` is the section player's. A
523
+ host that holds
524
+ the coordinator from `runtime-ready`, or passes its own as `coordinator`,
525
+ changes the profile with `coordinator.updateAssessment(...)`; the toolkit
526
+ applies its `assessment` property only to a coordinator it owns. The
527
+ `calculator-pnp` section demo composes the owned-coordinator form.
528
+
529
+ The toolkit builds its own coordinator at mount from `tools`, `enabled-tools`,
530
+ `assessment-id`, `accessibility`, `lazy-init`, `tool-config-strictness` and
531
+ `toolRegistry`. Its first content binds it: the section, or without one the
532
+ first item scope that registers. Content that arrives after one of those inputs
533
+ changed binds a coordinator rebuilt from the current values, and the toolbars
534
+ move to it. After that, a change to them is reported once in the console and
535
+ does not reach the coordinator, except a `toolRegistry` given to a toolkit that
536
+ had none, which the coordinator adopts in place; `tools.pnpEnforcement`,
537
+ `assessment` and `toolContextResolvers` apply to it at any
538
+ time. That
539
+ coordinator reports feature policy asked with no assessment bound only while
540
+ `tools.pnpEnforcement` is `on`: a toolkit given no `assessment` and no enforcement
541
+ has asked for no accommodation.
542
+
543
+ A toolkit nested in another inherits the outer one's coordinator when the outer
544
+ one has a coordinator by the time the inner one connects, which holds for both
545
+ mounted together. Otherwise the inner one builds its own and keeps it, and
546
+ reports an outer coordinator arriving later once in the console. `isolation`
547
+ `"force"` keeps a nested toolkit on its own coordinator by design.
548
+
549
+ Text-to-speech starts at the toolkit's first content, once the section composes
550
+ or the first item scope registers, and `coordinator.waitUntilReady()` waits for it. With `lazy-init` it starts at the first read-aloud instead, unless policy
551
+ grants it. A tool provider or text-to-speech that fails to start is a
552
+ recoverable framework error: the tool reports itself unavailable and the
553
+ assessment goes on. When policy grants the tool, through an item or district
554
+ requirement or a profile support, the failure is fatal, including one that a
555
+ later policy change grants.
556
+
396
557
  ## Tool Configuration Model
397
558
 
398
559
  The toolkit uses one canonical `tools` model with three concerns:
399
560
 
400
561
  - `policy`: allow/block constraints (global gates)
401
- - `placement`: where tools appear (`assessment`, `section`, `item`, `passage`, `rubric`, plus custom registered levels)
562
+ - `placement`: where tools appear (`section`, `item`, `passage`). Each tool
563
+ declares the levels it supports; `calculator` is item-only, and a tool placed
564
+ at a level it does not support fails validation, which throws under the
565
+ default `toolConfigStrictness: "error"`.
402
566
  - `providers`: provider/runtime options (calculator, textToSpeech, etc.)
403
567
 
404
568
  Example:
@@ -410,11 +574,9 @@ tools: {
410
574
  blocked: ['graph']
411
575
  },
412
576
  placement: {
413
- assessment: [],
414
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
577
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
415
578
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
416
- passage: ['textToSpeech'],
417
- rubric: []
579
+ passage: ['textToSpeech']
418
580
  },
419
581
  providers: {
420
582
  calculator: {
@@ -465,7 +627,8 @@ tools: {
465
627
  **Available Item-Level Tools:**
466
628
  - **TTS (Text-to-Speech)**: Reads the specific question/passage text
467
629
  - **Answer Eliminator**: Strikes through answer choices for that question
468
- - **Highlighter**: Highlights text within the item (future)
630
+ - **Calculator**: Basic, scientific or graphing calculator
631
+ - **Highlighter** (`annotationToolbar`): Highlights and annotates selected text; it opens from a text selection, outside the toolbars
469
632
 
470
633
  **Example Use Case:**
471
634
  A student uses answer eliminator on Question 3 to cross out choices B and D. When they navigate to Question 4, they see fresh, uneliminated choices. When they return to Question 3, their eliminations are restored.
@@ -477,38 +640,31 @@ Tools that **float above the entire assessment** and persist across questions:
477
640
  ```typescript
478
641
  tools: {
479
642
  placement: {
480
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
481
- },
482
- providers: {
483
- calculator: {
484
- enabled: true,
485
- provider: {
486
- runtime: { authFetcher: async () => { /* ... */ } }
487
- }
488
- }
643
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
489
644
  }
490
645
  }
491
646
  ```
492
647
 
648
+ The section player renders the section toolbar only when its `show-toolbar`
649
+ attribute is `true`; the attribute defaults to `false`.
650
+
493
651
  **Characteristics:**
494
652
  - **Scope**: Section-wide, shared across all questions
495
653
  - **Lifecycle**: Single instance initialized for entire section
496
- - **State**: Persistent (calculator history remains as you navigate)
654
+ - **State**: Persistent (a tool stays open and in place as you navigate)
497
655
  - **UI Pattern**: Draggable floating panels/overlays with z-index management
498
656
  - **State Persistence**: Global state maintained throughout section
499
657
 
500
658
  **Available Floating Tools:**
501
- - **Calculator**: Scientific/graphing calculator with computation history
502
- - **Graph**: Graphing tool for plotting functions
659
+ - **Graph**: Coordinate plane
503
660
  - **Periodic Table**: Interactive periodic table reference
504
661
  - **Protractor**: Angle measurement tool
505
662
  - **Ruler**: Linear measurement tool (metric/imperial)
506
663
  - **Line Reader**: Reading guide/masking overlay
507
- - **Magnifier**: Screen magnification tool
508
- - **Color Scheme**: High-contrast color adjustments
664
+ - **Color Scheme** (`theme`): High-contrast color adjustments
509
665
 
510
666
  **Example Use Case:**
511
- A student opens the calculator on Question 2, computes 45 × 12 = 540. They navigate to Question 7, and the calculator still shows their computation history. They can reference previous calculations across multiple questions without losing context.
667
+ A student opens the periodic table on Question 2 and moves it beside the passage. When they navigate to Question 7, it is still open where they left it.
512
668
 
513
669
  ### When to Use Each
514
670
 
@@ -529,15 +685,24 @@ Use **floating tools** when:
529
685
  Complete example showing both types:
530
686
 
531
687
  ```typescript
688
+ import {
689
+ createPackagedToolRegistry,
690
+ DEFAULT_TOOL_MODULE_LOADERS,
691
+ } from '@pie-players/pie-default-tool-loaders';
692
+
693
+ const toolRegistry = createPackagedToolRegistry({
694
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
695
+ });
532
696
  const coordinator = new ToolkitCoordinator({
533
697
  assessmentId: 'math-exam',
698
+ toolRegistry,
534
699
  tools: {
535
700
  placement: {
536
701
  // Contextual placement
537
702
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
538
703
  passage: ['textToSpeech'],
539
704
  // Section-level utilities
540
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
705
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler', 'theme']
541
706
  },
542
707
  providers: {
543
708
  calculator: {
@@ -561,16 +726,28 @@ const coordinator = new ToolkitCoordinator({
561
726
  });
562
727
  ```
563
728
 
564
- **Simple Default (All Tools Enabled):**
729
+ **A common placement:**
565
730
 
566
- For most use cases, simply enable all available tools:
731
+ A coordinator shows a tool only where `tools.placement` lists it. Its default
732
+ placement is empty, so a coordinator configured without one shows no tools, and
733
+ a profile grant does not place a tool either. This placement covers the
734
+ commonly used tools:
567
735
 
568
736
  ```typescript
737
+ import {
738
+ createPackagedToolRegistry,
739
+ DEFAULT_TOOL_MODULE_LOADERS,
740
+ } from '@pie-players/pie-default-tool-loaders';
741
+
742
+ const toolRegistry = createPackagedToolRegistry({
743
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
744
+ });
569
745
  const coordinator = new ToolkitCoordinator({
570
746
  assessmentId: 'my-assessment',
747
+ toolRegistry,
571
748
  tools: {
572
749
  placement: {
573
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
750
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
574
751
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
575
752
  passage: ['textToSpeech']
576
753
  },
@@ -588,12 +765,14 @@ const coordinator = new ToolkitCoordinator({
588
765
  ```
589
766
 
590
767
  The ToolkitCoordinator handles service initialization, provider management, and
591
- state coordination. With no calculator provider configuration, the existing
592
- Desmos implementation remains the default and preserves its legacy unkeyed load.
593
- That fallback is for client compatibility; it does not grant a Desmos license.
594
- Licensed deployments can supply `provider.init.apiKey` or
595
- `provider.runtime.authFetcher`. Runtime key delivery avoids committing the key
596
- but does not hide it from the browser's Desmos script request.
768
+ state coordination. With no calculator provider configuration, the Desmos
769
+ implementation remains the default. Desmos's CDN rejects a `calculator.js`
770
+ request without an `apiKey` (HTTP 403), so the Desmos calculator opens only when
771
+ the host supplies `provider.init.apiKey` or `provider.runtime.authFetcher`, or
772
+ has already loaded `window.Desmos`; the adapter's keyless request grants no
773
+ Desmos license either. Runtime key delivery avoids committing the key but does
774
+ not hide it from the browser's Desmos script request. The bundled open-source
775
+ implementation below needs no key.
597
776
 
598
777
  Select the separate GeoGebra implementation explicitly without changing the
599
778
  calculator capability, placement, or item policy:
@@ -630,7 +809,8 @@ tools: {
630
809
  providers: {
631
810
  textToSpeech: {
632
811
  enabled: true,
633
- backend: 'polly'
812
+ backend: 'server',
813
+ serverProvider: 'polly'
634
814
  }
635
815
  }
636
816
  }
@@ -646,7 +826,7 @@ You can still set `apiEndpoint` explicitly when your host route is not `/api/tts
646
826
 
647
827
  ### Inline TTS Speed Options
648
828
 
649
- Inline TTS speed buttons are configurable via `speedOptions` in provider settings.
829
+ Inline TTS speed buttons are configurable via `speedOptions` on the provider config.
650
830
 
651
831
  ```typescript
652
832
  tools: {
@@ -654,9 +834,7 @@ tools: {
654
834
  textToSpeech: {
655
835
  enabled: true,
656
836
  backend: "browser",
657
- settings: {
658
- speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
659
- }
837
+ speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
660
838
  }
661
839
  }
662
840
  }
@@ -673,13 +851,11 @@ tools: {
673
851
  enabled: true,
674
852
  backend: "server",
675
853
  serverProvider: "custom",
676
- settings: {
677
- speedOptions: [
678
- { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
679
- { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
680
- { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
681
- ]
682
- }
854
+ speedOptions: [
855
+ { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
856
+ { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
857
+ { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
858
+ ]
683
859
  }
684
860
  }
685
861
  }
@@ -704,25 +880,15 @@ tools: {
704
880
  - Speed choices are exposed as a `Playback speed` radio group: one option is
705
881
  always selected, and choosing the already-selected speed is a no-op.
706
882
  - If only one speed option remains, the speed group is hidden by default. Set
707
- `showSingleSpeedOption: true` in TTS settings to surface that one-option state.
708
-
709
- ### Runtime Fallback: Server TTS -> Browser TTS
710
-
711
- When server-backed playback fails at runtime (for example `503`, network outage,
712
- or synthesized asset fetch failure), `TTSService` now performs a one-time
713
- runtime fallback for that session:
714
-
715
- 1. Switches provider from server-backed implementation to browser speech synthesis.
716
- 2. Rebinds highlight callbacks to the browser provider.
717
- 3. Retries the same `speak()` request once.
883
+ `showSingleSpeedOption: true` on the provider config to surface that one-option state.
718
884
 
719
- This keeps the inline/passage TTS controls usable during transient backend
720
- incidents without requiring host-side reconfiguration.
885
+ ### Browser Fallback
721
886
 
722
- Telemetry emitted for observability:
723
-
724
- - `pie-tool-runtime-fallback` (fallback switch succeeded)
725
- - `pie-tool-runtime-fallback-error` (fallback switch failed)
887
+ The coordinator owns fallback to browser speech. When a server-backed provider
888
+ fails to initialize, the coordinator re-initializes TTS on the browser backend
889
+ and reports `pie-tool-init-fallback`. `TTSService.initialize` itself rejects
890
+ when its provider fails to start, and a playback failure after initialization
891
+ rejects that read without switching provider.
726
892
 
727
893
  `provider.runtime.authFetcher` is optional. Add it only when your host environment
728
894
  requires runtime auth material for TTS requests:
@@ -732,7 +898,8 @@ tools: {
732
898
  providers: {
733
899
  textToSpeech: {
734
900
  enabled: true,
735
- backend: 'polly',
901
+ backend: 'server',
902
+ serverProvider: 'polly',
736
903
  apiEndpoint: '/api/tts',
737
904
  provider: {
738
905
  runtime: {
@@ -747,6 +914,18 @@ tools: {
747
914
  }
748
915
  ```
749
916
 
917
+ The fetch completes before the first TTS request, and its result merges over the
918
+ provider config: a returned `authToken` is sent as `Authorization: Bearer <token>`
919
+ and returned `headers` with every synthesis request, and under
920
+ `includeAuthOnAssetFetch` the `Authorization` header also reaches the custom
921
+ transport's speech-mark and audio fetches. A failed fetch falls back to browser
922
+ speech and reports `pie-tool-init-fallback`.
923
+
924
+ For a server that authenticates by cookie on another origin, set `credentials:
925
+ "include"` on the `textToSpeech` provider config, beside
926
+ `includeAuthOnAssetFetch`. The cookie reaches speech-mark and audio fetches only
927
+ for origins in `assetOrigins`; unset, every TTS fetch keeps the browser default.
928
+
750
929
  ### Custom Transport via Server Proxy (SC-style)
751
930
 
752
931
  For custom backends that return URL assets (for example `{ audioContent, word }`),
@@ -782,27 +961,34 @@ SchoolCity is used as a host-configured integration example for custom transport
782
961
  Toolkit defaults still remain browser/standard providers unless the host explicitly
783
962
  configures custom server-backed TTS.
784
963
 
785
- ## Test Attempt Session Adapter (pie backend)
964
+ ## Test Attempt Session
786
965
 
787
- The toolkit exposes a canonical `TestAttemptSession` runtime and a deterministic adapter for pie backend activity payloads from `../../kds/pie-api-aws`.
966
+ The toolkit exposes a canonical `TestAttemptSession` runtime. The host maps its
967
+ backend's attempt payload into it and back out.
788
968
 
789
969
  ```typescript
790
970
  import {
791
- mapActivityToTestAttemptSession,
971
+ createNewTestAttemptSession,
792
972
  toItemSessionsRecord,
793
- buildActivitySessionPatchFromTestAttemptSession
973
+ upsertItemSessionFromPieSessionChange,
794
974
  } from "@pie-players/pie-assessment-toolkit";
795
975
 
796
- const testAttemptSession = mapActivityToTestAttemptSession({
797
- activityDefinition,
798
- activitySession
976
+ let testAttemptSession = createNewTestAttemptSession({
977
+ testAttemptSessionIdentifier: attempt.id,
978
+ assessmentId: assessment.id,
979
+ seed: attempt.id,
980
+ itemIdentifiers: assessment.itemIdentifiers,
981
+ });
982
+
983
+ // Record a PIE session change against its item
984
+ testAttemptSession = upsertItemSessionFromPieSessionChange(testAttemptSession, {
985
+ itemIdentifier: "q1",
986
+ pieSessionId: change.session.id,
987
+ session: change.session,
799
988
  });
800
989
 
801
990
  // Use in section-player handoff (same item session shape as item players expect)
802
991
  const itemSessions = toItemSessionsRecord(testAttemptSession);
803
-
804
- // Host-owned backend persistence payload
805
- const patch = buildActivitySessionPatchFromTestAttemptSession(testAttemptSession);
806
992
  ```
807
993
 
808
994
  ### Integration Boundary
@@ -826,21 +1012,17 @@ The persistence strategy works with the same `SectionControllerSessionState` sha
826
1012
 
827
1013
  - **ToolkitCoordinator**: ⭐ NEW - Centralized service orchestration
828
1014
  - **ElementToolStateStore**: ⭐ NEW - Element-level ephemeral tool state management
829
- - **ToolRegistry**: ⭐ NEW - Registry-based tool management with QTI 3.0 PNP support
830
- - **ToolPolicyEngine**: QTI 3.0 Personal Needs Profile and host policy decisions via registry-backed policy sources
1015
+ - **ToolRegistry**: ⭐ NEW - Registry-based tool management with AfA PNP 3.0 profile support
1016
+ - **ToolPolicyEngine**: AfA PNP 3.0 Personal Needs Profile and host policy decisions via registry-backed policy sources
831
1017
  - **ToolCoordinator**: Manages z-index layering and visibility for floating tools
832
1018
  - **HighlightCoordinator**: Separate highlight layers for TTS (temporary) and annotations (persistent)
833
1019
  - **TTSService**: Text-to-speech with QTI 3.0 catalog support
834
1020
  - **AccessibilityCatalogResolver**: QTI 3.0 accessibility catalog management
835
1021
  - **SSMLExtractor**: Automatic extraction of embedded `<speak>` tags
836
- - **ThemeProvider**: Consistent accessibility theming
837
1022
 
838
- ### ✅ QTI 3.0 Standard Access Features
1023
+ ### ✅ PNP Support Ids
839
1024
 
840
- - **95+ Standardized Features**: Complete QTI 3.0 / IMS AfA 3.0 accessibility features
841
- - **9 Feature Categories**: Visual, auditory, motor, cognitive, reading, navigation, linguistic, assessment
842
- - **Example Configurations**: Illustrative PNP profile examples (low vision, dyslexia, ADHD, etc.)
843
- - **Tool Mappings**: All 12 default tools map to standard QTI 3.0 features
1025
+ - **Support ids are tool ids**: A profile, district policy or item setting names a tool by its `toolId`, and an id no registered tool carries raises `tool-policy.unknownSupportId`. [Support ids](docs/TOOL_REGISTRY.md#support-ids) lists the AfA PNP 3.0 terms the packaged ids serve.
844
1026
 
845
1027
  ### ✅ Section Player Integration
846
1028
 
@@ -865,16 +1047,15 @@ export interface ToolkitCoordinatorConfig {
865
1047
  blocked?: string[];
866
1048
  };
867
1049
  placement?: {
868
- assessment?: string[];
869
1050
  section?: string[];
870
1051
  item?: string[];
871
1052
  passage?: string[];
872
- rubric?: string[];
873
1053
  };
874
1054
  providers?: {
875
1055
  textToSpeech?: {
876
1056
  enabled?: boolean;
877
- backend?: 'browser' | 'polly' | 'google' | 'server';
1057
+ backend?: 'browser' | 'server';
1058
+ serverProvider?: 'polly' | 'google' | 'custom';
878
1059
  defaultVoice?: string;
879
1060
  rate?: number;
880
1061
  };
@@ -903,14 +1084,9 @@ export interface ToolkitCoordinatorConfig {
903
1084
  ### Methods
904
1085
 
905
1086
  ```typescript
906
- // Get all services as a bundle
907
- const services = coordinator.getServiceBundle();
908
- // Returns: { ttsService, toolCoordinator, highlightCoordinator, elementToolStateStore, catalogResolver }
909
-
910
1087
  // Tool configuration
911
- coordinator.isToolEnabled('textToSpeech'); // Check if tool is enabled
912
1088
  coordinator.getToolConfig('textToSpeech'); // Get tool-specific config
913
- coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // Update tool config
1089
+ coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // Update tool config; replaces the tool's provider, which starts again on next use
914
1090
 
915
1091
  // Final teardown by the owner that constructed the coordinator
916
1092
  await coordinator.dispose();
@@ -1022,6 +1198,8 @@ unsubscribe();
1022
1198
  ### TTSService
1023
1199
 
1024
1200
  ```typescript
1201
+ import { BrowserTTSProvider, TTSService } from '@pie-players/pie-assessment-toolkit';
1202
+
1025
1203
  const ttsService = new TTSService();
1026
1204
 
1027
1205
  // Initialize with provider
@@ -1030,8 +1208,8 @@ await ttsService.initialize(new BrowserTTSProvider());
1030
1208
  // Set catalog resolver for SSML support
1031
1209
  ttsService.setCatalogResolver(catalogResolver);
1032
1210
 
1033
- // Playback
1034
- await ttsService.speak('Read this text', {
1211
+ // Playback: reads an element or a range, or the named card in its place
1212
+ await ttsService.speak(promptElement, {
1035
1213
  catalogId: 'prompt-001',
1036
1214
  language: 'en-US'
1037
1215
  });
@@ -1050,36 +1228,46 @@ await ttsService.updateSettings({
1050
1228
 
1051
1229
  ### ToolCoordinator
1052
1230
 
1053
- ```typescript
1054
- const toolCoordinator = new ToolCoordinator();
1055
-
1056
- // Register tools
1057
- toolCoordinator.registerTool('calculator', 'Calculator', element);
1231
+ The toolkit owns the coordinator. A tool element registers with the one in its
1232
+ runtime context and hands it the element once it renders:
1058
1233
 
1059
- // Manage visibility
1060
- toolCoordinator.showTool('calculator');
1061
- toolCoordinator.hideTool('calculator');
1062
- toolCoordinator.toggleTool('calculator');
1234
+ ```typescript
1235
+ import {
1236
+ createToolCoordinatorRegistration,
1237
+ ZIndexLayer,
1238
+ } from '@pie-players/pie-assessment-toolkit/tools/registration';
1063
1239
 
1064
- // Z-index management
1065
- toolCoordinator.bringToFront(element);
1240
+ const registration = createToolCoordinatorRegistration('Calculator', ZIndexLayer.TOOL);
1066
1241
 
1067
- // Check state
1068
- const isVisible = toolCoordinator.isToolVisible('calculator');
1242
+ // Re-registers when a republished context brings a new coordinator.
1243
+ $effect(() => registration.sync(coordinator, toolId));
1244
+ $effect(() => {
1245
+ if (coordinator && containerEl && toolId) {
1246
+ coordinator.updateToolElement(toolId, containerEl);
1247
+ }
1248
+ });
1249
+ onDestroy(() => registration.release());
1069
1250
  ```
1070
1251
 
1252
+ Visibility and stacking then go through the coordinator by scoped tool id:
1253
+ `showTool`, `hideTool`, `toggleTool`, `isToolVisible`, `bringToFront(element)`,
1254
+ and `getVisibleTools({ baseId })` / `hideAllTools({ baseId })` for one tool
1255
+ across scopes.
1256
+
1071
1257
  ### HighlightCoordinator
1072
1258
 
1073
1259
  ```typescript
1260
+ import { HighlightColor, HighlightCoordinator } from '@pie-players/pie-assessment-toolkit';
1261
+
1074
1262
  const highlightCoordinator = new HighlightCoordinator();
1075
1263
 
1076
1264
  // TTS highlights (temporary)
1077
- highlightCoordinator.highlightTTSWord(textNode, start, end);
1265
+ highlightCoordinator.highlightTTSWord([wordRange]); // one range per tree the word spans
1078
1266
  highlightCoordinator.highlightTTSSentence([range1, range2]);
1079
1267
  highlightCoordinator.clearTTS();
1080
1268
 
1081
1269
  // Annotation highlights (persistent)
1082
- const id = highlightCoordinator.addAnnotation(range, 'yellow');
1270
+ const id = highlightCoordinator.addAnnotation(range, HighlightColor.YELLOW);
1083
1271
  highlightCoordinator.removeAnnotation(id);
1084
1272
  ```
1085
1273
 
@@ -1104,6 +1292,8 @@ maintaining, across every scheme it ships.
1104
1292
  ### AccessibilityCatalogResolver
1105
1293
 
1106
1294
  ```typescript
1295
+ import { AccessibilityCatalogResolver } from '@pie-players/pie-assessment-toolkit';
1296
+
1107
1297
  const resolver = new AccessibilityCatalogResolver(
1108
1298
  assessment.accessibilityCatalogs,
1109
1299
  'en-US'
@@ -1167,6 +1357,8 @@ such as TTS may still call `getAlternative(...)` with a context built by
1167
1357
  ### SSMLExtractor
1168
1358
 
1169
1359
  ```typescript
1360
+ import { SSMLExtractor } from '@pie-players/pie-assessment-toolkit';
1361
+
1170
1362
  const extractor = new SSMLExtractor();
1171
1363
 
1172
1364
  // Extract from item config
@@ -1189,28 +1381,30 @@ that could not parse the markup left the video in the visible content, showing t
1189
1381
  accommodation to every learner regardless of eligibility.
1190
1382
 
1191
1383
  This package resolves and registers those cards through
1192
- `AccessibilityCatalogResolver` and the generic media helpers in
1193
- `catalog-media.ts`. Which card types mean what belongs to the capability that
1384
+ `AccessibilityCatalogResolver` and the generic media helpers it re-exports
1385
+ from `@pie-players/pie-players-shared/media`. Which card types mean what belongs to the capability that
1194
1386
  needs them — signing's card validators and its resolution rules live in
1195
1387
  `@pie-players/pie-tool-sign-language`, behind that capability's
1196
1388
  `requiresAuthoredContent`.
1197
1389
 
1198
1390
  ### Feature policy without a placement
1199
1391
 
1200
- `ToolPolicyEngine.decideFeature(featureId)` (and
1201
- `ToolkitCoordinator.decideFeaturePolicy(featureId)`) resolve one feature id
1392
+ `ToolPolicyEngine.decideFeature(featureId, scope?)` (and
1393
+ `ToolkitCoordinator.decideFeaturePolicy(featureId, scope?)`) resolve one feature id
1202
1394
  through `PnpPolicySource`'s six-level precedence, independent of any toolbar
1203
1395
  placement. Use it for capabilities that are not toolbar surfaces — signing is the
1204
1396
  first — where a placement-scoped `decide(...)` would answer the wrong question:
1205
1397
  absent because it was never placed, rather than absent because policy said no.
1398
+ An item scope applies that item's registered settings, the item restriction and
1399
+ requirement rungs; a decision without one applies no item's.
1206
1400
 
1207
- `createEmptyPersonalNeedsProfile()` is the only profile this package ships, and it
1208
- grants nothing. Which capabilities a deployment grants by default is a property
1401
+ This package ships no profile. Which capabilities a deployment grants by default is a property
1209
1402
  of the program rather than of a capability — TTS is a universal feature in one
1210
1403
  program and a documented accommodation in another — so it belongs in policy
1211
1404
  configuration alongside the district and test-administration levels. Hosts that
1212
1405
  want today's universal set take `createUniversalPersonalNeedsProfile()` from
1213
- `@pie-players/pie-default-tool-loaders`, which ships it as data.
1406
+ `@pie-players/pie-default-tool-loaders`, which ships it as data beside
1407
+ `createEmptyPersonalNeedsProfile()`.
1214
1408
 
1215
1409
  Nothing derives a profile from the registry any more. Doing so read registry
1216
1410
  membership as eligibility tier — registration means "policy-addressable", not
@@ -1232,18 +1426,26 @@ mounted surface elements immediately.
1232
1426
  The section player provides automatic ToolkitCoordinator integration:
1233
1427
 
1234
1428
  ```html
1235
- <pie-section-player id="player"></pie-section-player>
1429
+ <pie-section-player-splitpane id="player" section-id="section-1"></pie-section-player-splitpane>
1236
1430
 
1237
1431
  <script type="module">
1238
1432
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
1433
+ import {
1434
+ createPackagedToolRegistry,
1435
+ DEFAULT_TOOL_MODULE_LOADERS,
1436
+ } from '@pie-players/pie-default-tool-loaders';
1239
1437
 
1240
1438
  // Create coordinator
1439
+ const toolRegistry = createPackagedToolRegistry({
1440
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1441
+ });
1241
1442
  const coordinator = new ToolkitCoordinator({
1242
1443
  assessmentId: 'my-assessment',
1444
+ toolRegistry,
1243
1445
  tools: {
1244
1446
  providers: { textToSpeech: { enabled: true, backend: 'browser' } },
1245
1447
  placement: {
1246
- section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
1448
+ section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
1247
1449
  item: ['calculator', 'textToSpeech', 'answerEliminator'],
1248
1450
  passage: ['textToSpeech']
1249
1451
  }
@@ -1257,23 +1459,29 @@ The section player provides automatic ToolkitCoordinator integration:
1257
1459
 
1258
1460
  // Player automatically:
1259
1461
  // - Extracts services from coordinator
1260
- // - Generates section ID
1462
+ // - Scopes its runtime engine to `section-id`
1261
1463
  // - Provides runtime context to child components
1262
- // - Manages SSML extraction
1464
+ // - Registers item catalogs, including SSML catalogs a host extracted
1465
+ // into `config.extractedCatalogs` with `SSMLExtractor`
1263
1466
  // - Handles catalog lifecycle
1264
1467
  </script>
1265
1468
  ```
1266
1469
 
1267
1470
  ### Runtime Context Contract
1268
1471
 
1269
- The toolkit now exports a shared context key used by section-player and toolkit
1270
- components:
1472
+ Section-player and toolkit components share one runtime context. An element
1473
+ inside the toolkit's tree connects to it:
1271
1474
 
1272
1475
  ```typescript
1273
1476
  import {
1274
- assessmentToolkitRuntimeContext,
1477
+ connectToolRuntimeContext,
1275
1478
  type AssessmentToolkitRuntimeContext
1276
1479
  } from "@pie-players/pie-assessment-toolkit";
1480
+
1481
+ let runtime: AssessmentToolkitRuntimeContext | undefined;
1482
+ const disconnect = connectToolRuntimeContext(hostElement, (value) => {
1483
+ runtime = value;
1484
+ });
1277
1485
  ```
1278
1486
 
1279
1487
  `AssessmentToolkitRuntimeContext` carries ambient orchestration dependencies
@@ -1303,15 +1511,9 @@ player.section = mySection;
1303
1511
 
1304
1512
  // Internally creates:
1305
1513
  // new ToolkitCoordinator({
1306
- // assessmentId: 'anon_...', // auto-generated
1307
- // tools: {
1308
- // providers: { textToSpeech: { enabled: true, backend: 'browser' }, calculator: { enabled: true } },
1309
- // placement: {
1310
- // section: ['calculator', 'graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
1311
- // item: ['calculator', 'textToSpeech', 'answerEliminator'],
1312
- // passage: ['textToSpeech']
1313
- // }
1314
- // }
1514
+ // assessmentId: 'section-demo-direct', // or runtime.assessmentId
1515
+ // toolRegistry, // the player's toolRegistry, else the packaged registry
1516
+ // tools: player.runtime?.tools // no tools are placed when this is unset
1315
1517
  // })
1316
1518
  ```
1317
1519
 
@@ -1331,9 +1533,14 @@ import {
1331
1533
  createToolsConfig,
1332
1534
  ToolkitCoordinator
1333
1535
  } from "@pie-players/pie-assessment-toolkit";
1334
- import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
1536
+ import {
1537
+ createPackagedToolRegistry,
1538
+ DEFAULT_TOOL_MODULE_LOADERS,
1539
+ } from "@pie-players/pie-default-tool-loaders";
1335
1540
 
1336
- const toolRegistry = createPackagedToolRegistry();
1541
+ const toolRegistry = createPackagedToolRegistry({
1542
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1543
+ });
1337
1544
  const { config, diagnostics } = createToolsConfig({
1338
1545
  source: "host.bootstrap",
1339
1546
  strictness: "error",
@@ -1361,7 +1568,7 @@ const coordinator = new ToolkitCoordinator({
1361
1568
  Notes:
1362
1569
  - `providers.textToSpeech` is the canonical TTS provider key.
1363
1570
  - `providers.tts` is rejected by the validation contract.
1364
- - Custom tools can provide provider-level `sanitizeConfig` and `validateConfig` hooks.
1571
+ - A tool registration can declare `sanitizeConfig` and `validateConfig` hooks for its `providers.<toolId>` entry.
1365
1572
  - Hosts can react to framework errors via the `framework-error` DOM event,
1366
1573
  the `onFrameworkError(model)` callback prop, or by subscribing directly
1367
1574
  to the package-internal bus via
@@ -1375,132 +1582,63 @@ Notes:
1375
1582
 
1376
1583
  ## Section Runtime Engine (advanced)
1377
1584
 
1378
- The toolkit exposes a layered **section runtime engine** that consolidates
1379
- runtime resolution, FSM-driven stage progression, framework-error reporting,
1380
- DOM-event fan-out, and instrumentation into a single object hosts can mount
1381
- and dispose. The engine is what `<pie-section-player-…>` and
1382
- `<pie-assessment-toolkit>` use internally, and it is also the surface
1383
- custom hosts (or alternate layout shells) consume directly.
1384
-
1385
- ### Two import paths
1386
-
1387
- The engine ships with two deliberately separate entry points so consumers
1388
- pick the stability surface that matches their use case:
1389
-
1390
- - **Stable facade — `@pie-players/pie-assessment-toolkit/runtime/engine`.**
1391
- Narrow, semver-stable surface for hosts that want to mount, drive, and
1392
- dispose a section runtime. Re-exports `SectionRuntimeEngine`,
1393
- `SECTION_RUNTIME_ENGINE_KEY` (Svelte context), the cross-CE host
1394
- context (`sectionRuntimeEngineHostContext`), and the consumer-side
1395
- helper for that bridge (`connectSectionRuntimeEngineHostContext`).
1396
- The cross-CE host context exposes only a lifecycle handle; controller
1397
- methods stay on `SectionRuntimeEngine`.
1398
- - **Internal surface — `@pie-players/pie-assessment-toolkit/runtime/internal`.**
1399
- Wider, evolving surface for advanced hosts that need to construct an
1400
- engine manually, inspect FSM state, or build alternate fan-out paths.
1401
- Exposes `SectionEngineCore`, the four adapter bridges
1402
- (`createDomEventBridge`, `createFrameworkErrorBridge`,
1403
- `createCoordinatorBridge`, `createInstrumentationBridge`),
1404
- `FrameworkErrorBus`, cohort helpers,
1405
- and the `resolveRuntime` / `resolveToolsConfig` /
1406
- `resolveSectionEngineRuntimeState` helpers. Symbols here may change
1407
- between minor versions with a changeset note.
1408
-
1409
- ## Writing a capability package
1410
-
1411
- `@pie-players/pie-assessment-toolkit/tools/internal` is what a capability
1412
- package imports: the `ToolRegistration` contract, the surface and content
1413
- dependency types, `resolveToolTag` and the toolbar registration helpers. Same
1414
- stability contract as the other `*/internal` entry points — symbols may change
1415
- between minor versions with a changeset note.
1416
-
1417
- Import it rather than the package root: the root pulls in `ToolkitCoordinator`,
1418
- `TTSService` and the components, none of which a registration needs, and a
1419
- capability bundle that inlines them ends up with a second `ToolRegistry` class
1420
- that fails every `instanceof` across the host boundary. Mark the toolkit external
1421
- in the package's build with a pattern that covers subpaths, not a bare specifier.
1422
-
1423
- `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1424
- registration, a content resolver, its own custom element, and no edit to any
1425
- generic package. `packages/default-tool-loaders/README.md` covers how a
1426
- deployment then composes it in, and `docs/TOOL_REGISTRY.md` the registration and
1427
- host-surface contracts.
1428
-
1429
- ### Lifecycle emit coordination
1430
-
1431
- When `<pie-assessment-toolkit>` is nested inside a section-player layout,
1432
- the layout kernel publishes a lifecycle handle via
1433
- `sectionRuntimeEngineHostContext`. The toolkit detects that host
1434
- lifecycle owner and **suppresses its own external lifecycle DOM emits
1435
- and `onStageChange` callback** in favor of the layout CE host. From the
1436
- outside, one cohort yields one `pie-stage-change` /
1437
- `pie-loading-complete` chain on the layout CE host regardless of wrapper
1438
- depth. Controller-side
1439
- registration, content loading, session propagation, and persistence
1440
- remain toolkit-local through its own `SectionRuntimeEngine` instance.
1441
- A standalone `<pie-assessment-toolkit>` (no host context) emits from
1442
- its own engine.
1443
-
1444
- **Detection.** If a custom layout shell emits two `pie-stage-change`
1445
- events per stage transition (or two `pie-loading-complete` per cohort)
1446
- on the same layout CE — typically with two distinct `detail.runtimeId`
1447
- values — the shell has not published its engine via
1448
- `sectionRuntimeEngineHostContext`, so the wrapped
1449
- `<pie-assessment-toolkit>` falls back to its standalone lifecycle emit
1450
- path. Wire the bridge as shown below.
1585
+ The section runtime engine is the section player's stage chain: a pure FSM that
1586
+ turns the cohort a layout shows, the controller resolving and the readiness
1587
+ signals the layout derives into `pie-stage-change` and `pie-loading-complete`.
1588
+ The section player's layout kernel owns one per layout element, and it is the
1589
+ only stage emitter. `<pie-assessment-toolkit>` emits no stages: its
1590
+ `SectionControllerBinding` resolves the section's controller, which the toolkit
1591
+ announces with `toolkit-ready`, and the toolkit publishes framework errors.
1592
+
1593
+ ### Entry point
1594
+
1595
+ `@pie-players/pie-assessment-toolkit/runtime/engine` is the stable entry for a
1596
+ host that drives a section's stage chain. It carries `SectionRuntimeEngine` and
1597
+ the vocabulary of its surface: the `SectionEngineInput`s `dispatchInput` takes,
1598
+ the `SectionEngineOutput`s `subscribe` delivers, the `SectionEngineState` and
1599
+ `SectionEnginePhase` `getState` returns, the cohort helpers (`makeCohort`,
1600
+ `cohortsEqual`), the runtime config types with
1601
+ `resolveSectionEngineRuntimeState`, and the readiness signals. The engine core, its adapter and its DOM bridge have no
1602
+ entry: a host reaches them through the facade.
1603
+
1604
+ ### Stage chain
1605
+
1606
+ A cohort moves through `composed`, `engine-ready` and `interactive`, and ends at
1607
+ `disposed` on a cohort change or unmount. `engine-ready` follows the
1608
+ `section-controller-resolved` input, `interactive` the readiness signals that
1609
+ satisfy the readiness mode. A readiness update that reports `runtimeError`
1610
+ before `interactive` ends the chain: the first stage the cohort did not reach is
1611
+ `failed` and the rest up to `interactive` are `skipped`. The section player sets
1612
+ `runtimeError` from a non-recoverable `framework-error`, so a host waiting on
1613
+ `engine-ready` or `interactive` always hears an answer. `pie-loading-complete`
1614
+ fires once per cohort, when every item has loaded.
1451
1615
 
1452
1616
  ### Common-host wiring example
1453
1617
 
1454
- Most hosts never construct the engine directly — the section-player
1455
- layout CE and the toolkit CE handle it. Use the facade only when
1456
- building an alternate layout shell (e.g. a custom kernel host). The
1457
- shape mirrors what the section-player kernel does internally:
1618
+ Most hosts never construct the engine: the section-player layout elements do,
1619
+ and a host-built section layout takes its engine from
1620
+ `pie-section-player-kernel-host` (see the section player's
1621
+ [Custom layout authoring](../section-player/README.md#custom-layout-authoring)).
1622
+ Use the facade for a section renderer outside the section player. The shape
1623
+ mirrors the section-player kernel:
1458
1624
 
1459
1625
  ```ts
1460
- import { ContextProvider } from "@pie-players/pie-context";
1461
1626
  import {
1462
1627
  SectionRuntimeEngine,
1463
- sectionRuntimeEngineHostContext,
1464
- } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1465
- import {
1466
- FrameworkErrorBus,
1467
1628
  makeCohort,
1468
- } from "@pie-players/pie-assessment-toolkit/runtime/internal";
1629
+ } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1469
1630
 
1470
- const bus = new FrameworkErrorBus();
1471
1631
  const engine = new SectionRuntimeEngine();
1472
1632
 
1473
- // 1. Attach to the layout CE host. `sourceCe` is stamped onto every
1474
- // DOM event the engine dispatches and is required.
1475
- engine.attachHost({
1476
- host: layoutHostElement,
1477
- sourceCe: "my-custom-layout",
1478
- frameworkErrorBus: bus,
1479
- coordinator: toolkitCoordinator,
1480
- });
1633
+ // 1. Attach to the layout host. `sourceCe` is stamped onto every event.
1634
+ engine.attachHost({ host: layoutHostElement, sourceCe: "my-custom-layout" });
1481
1635
 
1482
- // 2. Publish a lifecycle handle on the layout CE host so any wrapped
1483
- // <pie-assessment-toolkit> suppresses duplicate external lifecycle
1484
- // emits. The toolkit still owns its controller registration/session
1485
- // plumbing locally.
1486
- const engineProvider = new ContextProvider(layoutHostElement, {
1487
- context: sectionRuntimeEngineHostContext,
1488
- initialValue: {
1489
- engine: {
1490
- getRuntimeId: () => engine.getRuntimeId(),
1491
- },
1492
- },
1636
+ // 2. (Optional) Subscribe to the output batches the DOM bridge dispatches.
1637
+ engine.subscribe((outputs) => {
1638
+ // `stage-change` and `loading-complete` outputs
1493
1639
  });
1494
- engineProvider.connect();
1495
1640
 
1496
- // 3. (Optional) Subscribe to the structured output stream — same set
1497
- // of outputs the DOM-event bridge fans out to the host element.
1498
- engine.subscribe((output) => {
1499
- // tap stage transitions, readiness updates, framework errors,
1500
- // instrumentation events
1501
- });
1502
-
1503
- // 4. Drive the engine. Use real `SectionEngineInput` shapes:
1641
+ // 3. Drive the engine with `SectionEngineInput`s.
1504
1642
  const cohort = makeCohort({ sectionId, attemptId });
1505
1643
  engine.dispatchInput({
1506
1644
  kind: "initialize",
@@ -1510,7 +1648,10 @@ engine.dispatchInput({
1510
1648
  itemCount,
1511
1649
  });
1512
1650
 
1513
- // On loading-progress / readiness signal updates:
1651
+ // When the toolkit's `section-ready` delivers the cohort's section controller:
1652
+ engine.dispatchInput({ kind: "section-controller-resolved" });
1653
+
1654
+ // On readiness signal updates:
1514
1655
  engine.dispatchInput({
1515
1656
  kind: "update-readiness-signals",
1516
1657
  signals: {
@@ -1519,33 +1660,44 @@ engine.dispatchInput({
1519
1660
  allLoadingComplete,
1520
1661
  runtimeError,
1521
1662
  },
1522
- loadedCount,
1523
1663
  itemCount,
1524
1664
  mode: "progressive",
1525
1665
  });
1526
1666
 
1527
- // On unmount:
1528
- engineProvider.disconnect();
1667
+ // On unmount; emits `disposed` for the active cohort.
1529
1668
  engine.dispose();
1530
1669
  ```
1531
1670
 
1532
- The DOM events `pie-stage-change`, `pie-loading-complete`, and
1533
- `framework-error` are dispatched on `host` automatically by the
1534
- adapter's `dom-event-bridge`. The canonical `onFrameworkError` callback
1535
- prop and the package-internal `FrameworkErrorBus` deliver each error
1536
- exactly once regardless of wrapper depth. The `framework-error` DOM
1537
- event on the layout CE host also delivers each error exactly once: the
1538
- section-player kernel intercepts the toolkit's bubbled emit at
1539
- `<pie-section-player-base>` and calls `event.stopPropagation()`, so the
1540
- layout host sees only the canonical engine-bridge emit. Direct
1541
- listeners on `<pie-assessment-toolkit>` itself still see the toolkit's
1542
- own emit (the toolkit dispatch reaches them before the kernel listener
1543
- runs). The single-emit contract is pinned by
1544
- `packages/section-player/tests/section-player-framework-error-dual-emit.test.ts`.
1545
- The layout host emits one `framework-error` DOM event per framework error.
1546
-
1547
- Hosts should listen to `pie-stage-change` (with the readiness detail also
1548
- available via the kernel's `selectReadiness()`) and `pie-loading-complete`.
1671
+ The adapter dispatches `pie-stage-change` and `pie-loading-complete` on `host`,
1672
+ bubbling and composed, as the toolkit dispatches its own events. Framework
1673
+ errors are not an engine output. The toolkit that owns the coordinator publishes
1674
+ each one once: one bubbling, composed `framework-error` event, which reaches the
1675
+ layout host and `document`, and one `onFrameworkError` call. A coordinator the
1676
+ host passes in reports through the same surfaces.
1677
+ `packages/section-player/tests/section-player-event-delivery.spec.ts` pins these
1678
+ counts.
1679
+
1680
+ ## Writing a capability package
1681
+
1682
+ `@pie-players/pie-assessment-toolkit/tools/registration` is the stable entry a
1683
+ capability package imports: the `ToolRegistration` contract, the
1684
+ `ToolProviderApi` its provider descriptor creates, the surface and content
1685
+ dependency types, the context predicates, `createToolElement`, `resolveToolTag`
1686
+ and the toolbar registration helpers. A renderer that puts registrations on
1687
+ screen uses the same entry for `createToolSurfaceHost` and
1688
+ `resolveContentCapabilities`.
1689
+
1690
+ Import it rather than the package root: the root pulls in `ToolkitCoordinator`,
1691
+ `TTSService` and the components, none of which a registration needs, and a
1692
+ capability bundle that inlines them ends up with a second `ToolRegistry` class
1693
+ that fails every `instanceof` across the host boundary. Mark the toolkit external
1694
+ in the package's build with a pattern that covers subpaths, not a bare specifier.
1695
+
1696
+ `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1697
+ registration, a content resolver, its own custom element, and no edit to any
1698
+ generic package. `packages/default-tool-loaders/README.md` covers how a
1699
+ deployment then composes it in, and `docs/TOOL_REGISTRY.md` the registration and
1700
+ host-surface contracts.
1549
1701
 
1550
1702
  ## State Separation: Tool State vs Session Data
1551
1703
 
@@ -1608,10 +1760,9 @@ Full TypeScript definitions included:
1608
1760
 
1609
1761
  ```typescript
1610
1762
  import type {
1611
- IToolkitCoordinator,
1612
- IElementToolStateStore,
1613
- ToolkitCoordinatorConfig,
1614
- ToolkitServiceBundle
1763
+ ToolkitCoordinatorApi,
1764
+ ElementToolStateStoreApi,
1765
+ ToolkitCoordinatorConfig
1615
1766
  } from '@pie-players/pie-assessment-toolkit';
1616
1767
  ```
1617
1768
 
@@ -1623,7 +1774,7 @@ from tool configuration. Two sanitization layers apply:
1623
1774
 
1624
1775
  - **Item / passage markup** - sanitized by default in
1625
1776
  `pie-item-player`. See
1626
- [pie-item-player README](./README.md#content-trust-boundary)
1777
+ [pie-item-player README](../item-player/README.md#content-trust-boundary)
1627
1778
  for the `trust-markup` opt-out and the `sanitizeMarkup` override.
1628
1779
  As a post-sanitization step, every authored `<img>` outside a `pie-*`
1629
1780
  custom element is wrapped in `<span class="pie-image-scroll">` so
@@ -1645,7 +1796,7 @@ from tool configuration. Two sanitization layers apply:
1645
1796
 
1646
1797
  ## Related Documentation
1647
1798
 
1648
- - **[Tool Registry Architecture](docs/TOOL_REGISTRY.md)** - ⭐ NEW - Registry-based tool management and QTI 3.0 PNP support
1799
+ - **[Tool Registry Architecture](docs/TOOL_REGISTRY.md)** - ⭐ NEW - Registry-based tool management and AfA PNP 3.0 profile support
1649
1800
  - **[PNP Configuration Guide](docs/PNP_CONFIGURATION.md)** - ⭐ NEW - How to configure student profiles, district policies, and governance rules
1650
1801
  - [ToolkitCoordinator Architecture](../../docs/architecture/architecture.md#toolkitcoordinator-centralized-service-management) - Design decisions and patterns
1651
1802
  - [Section Player README](../section-player/README.md) - Section player integration