@pie-players/pie-assessment-toolkit 0.3.74 → 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 (228) hide show
  1. package/README.md +349 -313
  2. package/dist/attempt/TestSession.d.ts +1 -20
  3. package/dist/attempt/TestSession.js +8 -99
  4. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  5. package/dist/components/ItemScope.custom-element.js +1 -0
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  10. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  13. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  14. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  15. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  16. package/dist/components/item-scope-element.d.ts +2 -0
  17. package/dist/components/item-scope-element.js +2 -0
  18. package/dist/context/assessment-toolkit-context.d.ts +27 -6
  19. package/dist/index.d.ts +22 -47
  20. package/dist/index.js +12 -36
  21. package/dist/policy/core/PolicySource.d.ts +1 -1
  22. package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
  23. package/dist/policy/core/ToolPolicyEngine.js +136 -46
  24. package/dist/policy/core/compose-decision.d.ts +28 -7
  25. package/dist/policy/core/compose-decision.js +75 -54
  26. package/dist/policy/core/decision-types.d.ts +65 -34
  27. package/dist/policy/core/feature-decision.d.ts +16 -9
  28. package/dist/policy/core/feature-decision.js +14 -8
  29. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  30. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  31. package/dist/policy/core/provenance.d.ts +9 -4
  32. package/dist/policy/core/provenance.js +1 -1
  33. package/dist/policy/engine.d.ts +7 -23
  34. package/dist/policy/engine.js +5 -20
  35. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  36. package/dist/policy/sources/PnpPolicySource.js +117 -118
  37. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  38. package/dist/runtime/SectionControllerBinding.js +450 -0
  39. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  40. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  41. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  42. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  43. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  44. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  45. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  46. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  47. package/dist/runtime/catalog-context.d.ts +19 -0
  48. package/dist/runtime/catalog-context.js +28 -0
  49. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  50. package/dist/runtime/composition-emit-scheduler.js +9 -15
  51. package/dist/runtime/content-language.d.ts +32 -0
  52. package/dist/runtime/content-language.js +46 -0
  53. package/dist/runtime/content-region.d.ts +17 -0
  54. package/dist/runtime/content-region.js +29 -0
  55. package/dist/runtime/content-registration.d.ts +13 -0
  56. package/dist/runtime/content-registration.js +25 -0
  57. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  58. package/dist/runtime/core/SectionEngineCore.js +2 -2
  59. package/dist/runtime/core/cohort.d.ts +1 -1
  60. package/dist/runtime/core/cohort.js +1 -1
  61. package/dist/runtime/core/engine-input.d.ts +18 -30
  62. package/dist/runtime/core/engine-input.js +11 -12
  63. package/dist/runtime/core/engine-output.d.ts +16 -40
  64. package/dist/runtime/core/engine-output.js +9 -21
  65. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  66. package/dist/runtime/core/engine-readiness.js +8 -41
  67. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  68. package/dist/runtime/core/engine-resolver.js +18 -43
  69. package/dist/runtime/core/engine-state.d.ts +28 -36
  70. package/dist/runtime/core/engine-state.js +18 -21
  71. package/dist/runtime/core/engine-transition.d.ts +14 -19
  72. package/dist/runtime/core/engine-transition.js +71 -51
  73. package/dist/runtime/engine.d.ts +16 -19
  74. package/dist/runtime/engine.js +12 -19
  75. package/dist/runtime/page-warnings.d.ts +13 -0
  76. package/dist/runtime/page-warnings.js +22 -0
  77. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  78. package/dist/runtime/policy-input-forwarding.js +25 -0
  79. package/dist/runtime/registration-events.d.ts +11 -0
  80. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  81. package/dist/runtime/runtime-event-claim.js +15 -0
  82. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  83. package/dist/runtime/shell-event-bridge.js +127 -0
  84. package/dist/runtime/shell-registration.d.ts +73 -0
  85. package/dist/runtime/shell-registration.js +95 -0
  86. package/dist/runtime/shell-scope.d.ts +68 -0
  87. package/dist/runtime/shell-scope.js +188 -0
  88. package/dist/runtime/tool-host-contract.d.ts +8 -10
  89. package/dist/runtime/tool-host-contract.js +11 -11
  90. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  91. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  92. package/dist/services/ElementToolStateStore.d.ts +14 -10
  93. package/dist/services/ElementToolStateStore.js +37 -13
  94. package/dist/services/HighlightCoordinator.d.ts +21 -21
  95. package/dist/services/HighlightCoordinator.js +431 -325
  96. package/dist/services/RangeSerializer.d.ts +10 -1
  97. package/dist/services/RangeSerializer.js +87 -14
  98. package/dist/services/TTSService.d.ts +117 -70
  99. package/dist/services/TTSService.js +668 -578
  100. package/dist/services/ToolCoordinator.d.ts +43 -60
  101. package/dist/services/ToolCoordinator.js +164 -206
  102. package/dist/services/ToolRegistry.d.ts +74 -57
  103. package/dist/services/ToolRegistry.js +104 -114
  104. package/dist/services/ToolkitCoordinator.d.ts +183 -189
  105. package/dist/services/ToolkitCoordinator.js +477 -431
  106. package/dist/services/audio-handoff.d.ts +6 -7
  107. package/dist/services/audio-handoff.js +20 -14
  108. package/dist/services/catalog-owner.d.ts +0 -1
  109. package/dist/services/framework-error-bus.d.ts +3 -6
  110. package/dist/services/framework-error-bus.js +3 -6
  111. package/dist/services/framework-error.d.ts +22 -7
  112. package/dist/services/framework-error.js +9 -7
  113. package/dist/services/interfaces.d.ts +158 -129
  114. package/dist/services/section-session-assignment.d.ts +21 -0
  115. package/dist/services/section-session-assignment.js +82 -0
  116. package/dist/services/spoken-audio-cards.d.ts +0 -2
  117. package/dist/services/spoken-audio-cards.js +0 -2
  118. package/dist/services/tool-config-defaults.d.ts +3 -3
  119. package/dist/services/tool-config-defaults.js +3 -3
  120. package/dist/services/tool-config-validation.d.ts +4 -5
  121. package/dist/services/tool-config-validation.js +36 -61
  122. package/dist/services/tool-context.d.ts +0 -8
  123. package/dist/services/tool-context.js +0 -12
  124. package/dist/services/tool-icons.d.ts +4 -3
  125. package/dist/services/tool-icons.js +4 -3
  126. package/dist/services/tool-instance-id.d.ts +0 -1
  127. package/dist/services/tool-instance-id.js +0 -6
  128. package/dist/services/tool-providers/TTSToolProvider.d.ts +18 -19
  129. package/dist/services/tool-providers/TTSToolProvider.js +37 -34
  130. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  131. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
  132. package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
  133. package/dist/services/tool-request.d.ts +27 -12
  134. package/dist/services/tool-request.js +24 -14
  135. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  136. package/dist/services/toolbar-decided-tools.js +28 -0
  137. package/dist/services/tools-config-normalizer.d.ts +17 -22
  138. package/dist/services/tools-config-normalizer.js +2 -10
  139. package/dist/services/tts/browser-provider.d.ts +38 -2
  140. package/dist/services/tts/browser-provider.js +276 -140
  141. package/dist/services/tts/flat-tree.d.ts +71 -0
  142. package/dist/services/tts/flat-tree.js +269 -0
  143. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  144. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  145. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  146. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  147. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  148. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  149. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +12 -4
  150. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  151. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  152. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  153. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  154. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  155. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  156. package/dist/services/tts/math-aware-text-processing.js +32 -18
  157. package/dist/services/tts/math-control-names.d.ts +21 -0
  158. package/dist/services/tts/math-control-names.js +142 -0
  159. package/dist/services/tts/math-speech.d.ts +11 -0
  160. package/dist/services/tts/math-speech.js +51 -18
  161. package/dist/services/tts/provider-options.d.ts +41 -0
  162. package/dist/services/tts/provider-options.js +1 -0
  163. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  164. package/dist/services/tts/ssml/spoken-text.js +29 -0
  165. package/dist/services/tts/start-failure.d.ts +11 -0
  166. package/dist/services/tts/start-failure.js +19 -0
  167. package/dist/services/tts/text-processing.d.ts +9 -0
  168. package/dist/services/tts/text-processing.js +46 -33
  169. package/dist/services/tts/text-segmentation.d.ts +7 -0
  170. package/dist/services/tts/text-segmentation.js +47 -0
  171. package/dist/services/tts-runtime-config.d.ts +25 -17
  172. package/dist/services/tts-runtime-config.js +16 -95
  173. package/dist/tools/content-capability-resolution.d.ts +7 -9
  174. package/dist/tools/content-capability-resolution.js +8 -45
  175. package/dist/tools/registration.d.ts +58 -0
  176. package/dist/tools/registration.js +54 -0
  177. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  178. package/dist/tools/tool-surface-host.d.ts +6 -10
  179. package/dist/tools/tool-surface-host.js +44 -42
  180. package/dist/tools/tool-tag-map.d.ts +5 -15
  181. package/dist/tools/tool-tag-map.js +6 -20
  182. package/dist/utils/structural-equality.d.ts +11 -0
  183. package/dist/utils/structural-equality.js +47 -0
  184. package/package.json +13 -29
  185. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  186. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  187. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  188. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  189. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  190. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  191. package/dist/context/runtime-context-consumer.d.ts +0 -13
  192. package/dist/context/runtime-context-consumer.js +0 -18
  193. package/dist/item-loader.d.ts +0 -44
  194. package/dist/item-loader.js +0 -61
  195. package/dist/policy/core/engine-context.d.ts +0 -29
  196. package/dist/policy/core/engine-context.js +0 -27
  197. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  198. package/dist/policy/core/policy-source-tag.js +0 -28
  199. package/dist/policy/internal.d.ts +0 -25
  200. package/dist/policy/internal.js +0 -25
  201. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  202. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  203. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  204. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  205. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  206. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  207. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  208. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  209. package/dist/runtime/engine-context.d.ts +0 -39
  210. package/dist/runtime/engine-context.js +0 -31
  211. package/dist/runtime/internal.d.ts +0 -43
  212. package/dist/runtime/internal.js +0 -45
  213. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  214. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  215. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  216. package/dist/runtime/stage-emit-gate.js +0 -82
  217. package/dist/services/I18nService.d.ts +0 -47
  218. package/dist/services/I18nService.js +0 -84
  219. package/dist/services/ThemeProvider.d.ts +0 -95
  220. package/dist/services/ThemeProvider.js +0 -334
  221. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  222. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  223. package/dist/services/pnp-standard-features.d.ts +0 -238
  224. package/dist/services/pnp-standard-features.js +0 -338
  225. package/dist/tools/client.d.ts +0 -10
  226. package/dist/tools/client.js +0 -10
  227. package/dist/tools/internal.d.ts +0 -44
  228. package/dist/tools/internal.js +0 -39
package/README.md CHANGED
@@ -15,7 +15,7 @@ The examples build their tool registry with `createPackagedToolRegistry()` from
15
15
  in its own dependencies. The section player depends on it too, but strict
16
16
  installers such as pnpm expose only the packages a host declares.
17
17
 
18
- Server-backed TTS (`backend: "polly"`, `"google"` or `"server"`) loads
18
+ Server-backed TTS (`backend: "server"`) loads
19
19
  `@pie-players/tts-client-server`, a dependency of
20
20
  `@pie-players/pie-default-tool-loaders` whose TTS registration imports it on
21
21
  first use. If it fails to load, TTS initialization reports a `provider-init`
@@ -52,10 +52,15 @@ player.toolCoordinator = toolCoordinator;
52
52
 
53
53
  **After** (coordinator orchestrates):
54
54
  ```typescript
55
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
55
+ import {
56
+ createPackagedToolRegistry,
57
+ DEFAULT_TOOL_MODULE_LOADERS,
58
+ } from '@pie-players/pie-default-tool-loaders';
56
59
 
57
60
  // Create one coordinator with configuration
58
- const toolRegistry = createPackagedToolRegistry();
61
+ const toolRegistry = createPackagedToolRegistry({
62
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
63
+ });
59
64
  const toolkitCoordinator = new ToolkitCoordinator({
60
65
  assessmentId: 'my-assessment',
61
66
  toolRegistry,
@@ -113,9 +118,9 @@ provider path is the item-player loader config:
113
118
 
114
119
  ### Toolkit-Owned Canonical Event Stream
115
120
 
116
- - `pie-toolkit-stage-change`
117
121
  - `pie-toolkit-runtime-owned`
118
122
  - `pie-toolkit-runtime-inherited`
123
+ - `pie-toolkit-runtime-ready`
119
124
  - `pie-toolkit-ready`
120
125
  - `pie-toolkit-section-ready`
121
126
  - `pie-toolkit-framework-error`
@@ -139,20 +144,20 @@ See the [ToolkitCoordinator section in the architecture overview](../../docs/arc
139
144
  1. **Centralized Coordination**: ToolkitCoordinator orchestrates all services
140
145
  2. **Composable Services**: Import only what you need (or use coordinator for convenience)
141
146
  3. **No Framework Lock-in**: Works with any JavaScript framework
142
- 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)
143
148
  5. **Standard Contracts**: Well-defined event types for component communication
144
149
  6. **Element-Level Granularity**: Tool state tracked per PIE element, not per item
145
150
  7. **State Separation**: Tool state (ephemeral) separate from PIE session data (persistent)
146
151
 
147
- ## Configuration tiers: easy attribute + sophisticated `runtime`
152
+ ## Configuration: attributes and a constructed coordinator
148
153
 
149
- This package and `@pie-players/pie-section-player` follow a deliberate
150
- two-tier configuration model. The same knob can usually be set in either
151
- 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.
152
157
 
153
- ### When to use each tier
158
+ ### Element inputs and a constructed coordinator
154
159
 
155
- - **Easy tier — top-level CE attributes / properties.** Use these for the
160
+ - **Element attributes / properties.** Use these for the
156
161
  common cases that are static for the lifetime of the player or that hosts
157
162
  want to set declaratively in HTML / templating frameworks. Example:
158
163
 
@@ -164,16 +169,21 @@ tier; the choice is about ergonomics, not capability.
164
169
  ></pie-assessment-toolkit>
165
170
  ```
166
171
 
167
- - **Sophisticated tier — passing a constructed `ToolkitCoordinator` (or a
172
+ - **A constructed `ToolkitCoordinator` (or a
168
173
  `runtime` object on consumer CEs).** Use this for advanced cases: composed
169
- configuration, dynamic overrides, runtime mutation, fields without a
170
- 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
171
176
  object passed by reference. Example:
172
177
 
173
178
  ```ts
174
- import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
179
+ import {
180
+ createPackagedToolRegistry,
181
+ DEFAULT_TOOL_MODULE_LOADERS,
182
+ } from "@pie-players/pie-default-tool-loaders";
175
183
 
176
- const toolRegistry = createPackagedToolRegistry();
184
+ const toolRegistry = createPackagedToolRegistry({
185
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
186
+ });
177
187
  const coordinator = new ToolkitCoordinator({
178
188
  assessmentId: "my-assessment",
179
189
  toolRegistry,
@@ -192,38 +202,32 @@ Top-level attributes use kebab-case (`assessment-id`,
192
202
  `tool-config-strictness`). Section-player runtime configuration is grouped
193
203
  under the `runtime` object instead of duplicated as top-level props.
194
204
 
195
- ### Precedence rule
205
+ ### One tier per input
196
206
 
197
- The configuration object owns runtime fields. Section-player layout attributes
198
- 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.
199
213
 
200
- 1. Use `runtime.<key>` for player, tool, accessibility, coordinator, env,
201
- isolation, and runtime factories.
202
- 2. Use top-level layout attributes for section identity and layout controls.
203
- 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`.
204
217
 
205
- ### Canonical tier-1 attribute set
218
+ ### Canonical attribute set
206
219
 
207
- The tier-1 attribute set is the same shape across
208
- `pie-assessment-toolkit`, `pie-section-player-base`, and the
209
- `pie-section-player-*` layout elements (locked in M5). Every tier-1
210
- surface obeys the strict mirror rule:
211
-
212
- ```
213
- kebab-attribute ↔ camelCaseProp ↔ runtime.<sameCamelCaseKey>
214
- ```
215
-
216
- Common members include:
217
-
218
- - Identity: `assessment-id`, `section-id`, `attempt-id`
220
+ - Identity: `assessment-id` on the toolkit, `section-id`, `attempt-id`
219
221
  - Runtime config on section-player CEs: `runtime`
220
222
  - Toolkit-only object properties: `tools`, `tool-registry`, `coordinator`,
221
223
  `accessibility`
224
+ - Interface: `nds-icons`, `locale`
222
225
  - Diagnostics: `tool-config-strictness`, `debug`. Framework-error
223
- 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
224
228
  `framework-error` DOM event dispatched on the layout CE host.
225
229
 
226
- Documented exceptions to the mirror rule:
230
+ Inputs outside the runtime config:
227
231
 
228
232
  - Identity (`section-id`, `attempt-id`, `section`): per-attempt host
229
233
  state, not configuration.
@@ -235,17 +239,15 @@ Documented exceptions to the mirror rule:
235
239
  `runtime.tools.placement.{item,passage}` directly.
236
240
  - Runtime-only keys on the section-player layout CEs
237
241
  (`createSectionController`, `isolation`): accepted only via
238
- `runtime.<key>`. `<pie-assessment-toolkit>`
239
- itself keeps `createSectionController` and `isolation` as JS-only
240
- props (no kebab-attribute surface): section-player layouts forward
241
- `runtime.isolation` and `runtime.createSectionController` to the
242
- wrapped toolkit via property bindings; standalone hosts that need
243
- to override coordinator inheritance should pass an explicit
244
- `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.
245
247
 
246
- ### When to add a tier-1 attribute
248
+ ### When to add an attribute
247
249
 
248
- Add a tier-1 attribute only if all of the following hold:
250
+ Add an attribute only if all of the following hold:
249
251
 
250
252
  - It is a common case that hosts set without composing a `ToolkitCoordinator`
251
253
  / `runtime` object.
@@ -263,10 +265,15 @@ Otherwise expose it through the configuration object only.
263
265
 
264
266
  ```typescript
265
267
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
266
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
268
+ import {
269
+ createPackagedToolRegistry,
270
+ DEFAULT_TOOL_MODULE_LOADERS,
271
+ } from '@pie-players/pie-default-tool-loaders';
267
272
 
268
273
  // Create coordinator with configuration
269
- const toolRegistry = createPackagedToolRegistry();
274
+ const toolRegistry = createPackagedToolRegistry({
275
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
276
+ });
270
277
  const coordinator = new ToolkitCoordinator({
271
278
  assessmentId: 'demo-assessment',
272
279
  toolRegistry,
@@ -324,6 +331,7 @@ Behavior pins (PIE-512 Phase D):
324
331
 
325
332
  - Subscribe **after** the first `getOrCreateSectionController(...)` resolves; calling subscribe before any cohort exists throws.
326
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.
327
335
  - Subscribing the **same listener function** twice replaces the first subscription (filter args from the second call win).
328
336
  - A listener that throws is caught and `console.warn`-logged; the throw does not interrupt fan-out to other listeners.
329
337
 
@@ -357,7 +365,7 @@ What this means in practice for typed integrations:
357
365
 
358
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.**
359
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.
360
- - **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.
361
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.
362
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`.
363
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.
@@ -425,27 +433,41 @@ const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
425
433
  await ttsService.initialize(new BrowserTTSProvider());
426
434
  ttsService.setCatalogResolver(catalogResolver);
427
435
 
428
- // Pass services individually
429
- player.ttsService = ttsService;
430
- player.toolCoordinator = toolCoordinator;
431
- // ...
436
+ // Use them from host-built UI
432
437
  ```
433
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
+
434
444
  ### Without a Section Player
435
445
 
436
446
  `<pie-assessment-toolkit>` needs no section. Bind none and it provides tools,
437
447
  policy and services to the item toolbars and item players inside it, which is
438
- how the toolkit accompanies a plain item player:
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:
439
451
 
440
452
  ```html
441
- <pie-assessment-toolkit pnp-enforcement="on">
442
- <pie-item-toolbar item-id="q1"></pie-item-toolbar>
443
- <pie-item-player></pie-item-player>
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>
444
460
  </pie-assessment-toolkit>
445
461
  ```
446
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
+
447
468
  ```typescript
448
469
  import '@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element';
470
+ import '@pie-players/pie-assessment-toolkit/components/item-scope-element';
449
471
  import '@pie-players/pie-assessment-toolkit/components/item-toolbar-element';
450
472
  import '@pie-players/pie-item-player';
451
473
  import {
@@ -458,42 +480,80 @@ const toolRegistry = createPackagedToolRegistry({
458
480
  toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
459
481
  });
460
482
 
461
- toolkit.tools = { placement: { item: ['calculator'] } };
483
+ const toolkit = document.createElement('pie-assessment-toolkit');
484
+ toolkit.tools = {
485
+ pnpEnforcement: 'on',
486
+ placement: { item: ['textToSpeech', 'calculator'] },
487
+ };
462
488
  toolkit.toolRegistry = toolRegistry;
463
489
  toolkit.toolContextResolvers = toolContextResolvers;
464
490
  toolkit.assessment = { id: 'a1', personalNeedsProfile: { supports: ['calculator'] } };
465
491
 
466
- toolbar.item = item;
467
- toolbar.toolRegistry = toolRegistry; // the toolbar does not read the toolkit's
468
- toolbar.scopeElement = player; // the content answerEliminator acts on
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);
469
502
  ```
470
503
 
471
- A registry built without `toolModuleLoaders` renders toolbar buttons whose tool
472
- elements never load. `textToSpeech` reads the region an item shell publishes,
473
- which only the section player's cards provide, so it has no reading target in
474
- this composition.
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.
475
515
 
476
516
  A profile change is a new `assessment` value; the toolbars re-derive on the
477
- policy change it emits. Readiness events are section events: `toolkit-ready`,
478
- `section-ready` and every stage, `composed` included, wait for a bound section,
479
- and the toolkit builds its own coordinator only after it mounts. A host that needs a
480
- readiness point constructs the `ToolkitCoordinator`, passes it as
481
- `coordinator`, awaits `coordinator.waitUntilReady()`, and changes the profile
482
- with `coordinator.updateAssessment(...)`, since the toolkit applies its
483
- `assessment` property to a coordinator it owns. The `calculator-pnp` section
484
- demo composes the owned-coordinator form.
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.
485
528
 
486
529
  The toolkit builds its own coordinator at mount from `tools`, `enabled-tools`,
487
530
  `assessment-id`, `accessibility`, `lazy-init`, `tool-config-strictness` and
488
- `toolRegistry`. A section that arrives after one of those changed initializes
489
- with a coordinator rebuilt from the current values, and the toolbars move to
490
- it. After a section has initialized, a change to them is reported once in the
491
- console and does not reach the coordinator; `pnp-enforcement`, `assessment`,
492
- `currentItemRef` and `toolContextResolvers` apply to it at any time. That
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
493
539
  coordinator reports feature policy asked with no assessment bound only while
494
- `pnp-enforcement` is `on`: a toolkit given no `assessment` and no enforcement
540
+ `tools.pnpEnforcement` is `on`: a toolkit given no `assessment` and no enforcement
495
541
  has asked for no accommodation.
496
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
+
497
557
  ## Tool Configuration Model
498
558
 
499
559
  The toolkit uses one canonical `tools` model with three concerns:
@@ -625,9 +685,14 @@ Use **floating tools** when:
625
685
  Complete example showing both types:
626
686
 
627
687
  ```typescript
628
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
688
+ import {
689
+ createPackagedToolRegistry,
690
+ DEFAULT_TOOL_MODULE_LOADERS,
691
+ } from '@pie-players/pie-default-tool-loaders';
629
692
 
630
- const toolRegistry = createPackagedToolRegistry();
693
+ const toolRegistry = createPackagedToolRegistry({
694
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
695
+ });
631
696
  const coordinator = new ToolkitCoordinator({
632
697
  assessmentId: 'math-exam',
633
698
  toolRegistry,
@@ -661,14 +726,22 @@ const coordinator = new ToolkitCoordinator({
661
726
  });
662
727
  ```
663
728
 
664
- **Simple Default (All Tools Enabled):**
729
+ **A common placement:**
665
730
 
666
- 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:
667
735
 
668
736
  ```typescript
669
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
737
+ import {
738
+ createPackagedToolRegistry,
739
+ DEFAULT_TOOL_MODULE_LOADERS,
740
+ } from '@pie-players/pie-default-tool-loaders';
670
741
 
671
- const toolRegistry = createPackagedToolRegistry();
742
+ const toolRegistry = createPackagedToolRegistry({
743
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
744
+ });
672
745
  const coordinator = new ToolkitCoordinator({
673
746
  assessmentId: 'my-assessment',
674
747
  toolRegistry,
@@ -736,7 +809,8 @@ tools: {
736
809
  providers: {
737
810
  textToSpeech: {
738
811
  enabled: true,
739
- backend: 'polly'
812
+ backend: 'server',
813
+ serverProvider: 'polly'
740
814
  }
741
815
  }
742
816
  }
@@ -752,7 +826,7 @@ You can still set `apiEndpoint` explicitly when your host route is not `/api/tts
752
826
 
753
827
  ### Inline TTS Speed Options
754
828
 
755
- Inline TTS speed buttons are configurable via `speedOptions` in provider settings.
829
+ Inline TTS speed buttons are configurable via `speedOptions` on the provider config.
756
830
 
757
831
  ```typescript
758
832
  tools: {
@@ -760,9 +834,7 @@ tools: {
760
834
  textToSpeech: {
761
835
  enabled: true,
762
836
  backend: "browser",
763
- settings: {
764
- speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
765
- }
837
+ speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
766
838
  }
767
839
  }
768
840
  }
@@ -779,13 +851,11 @@ tools: {
779
851
  enabled: true,
780
852
  backend: "server",
781
853
  serverProvider: "custom",
782
- settings: {
783
- speedOptions: [
784
- { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
785
- { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
786
- { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
787
- ]
788
- }
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
+ ]
789
859
  }
790
860
  }
791
861
  }
@@ -810,25 +880,15 @@ tools: {
810
880
  - Speed choices are exposed as a `Playback speed` radio group: one option is
811
881
  always selected, and choosing the already-selected speed is a no-op.
812
882
  - If only one speed option remains, the speed group is hidden by default. Set
813
- `showSingleSpeedOption: true` in TTS settings to surface that one-option state.
814
-
815
- ### Runtime Fallback: Server TTS -> Browser TTS
816
-
817
- When server-backed playback fails at runtime (for example `503`, network outage,
818
- or synthesized asset fetch failure), `TTSService` now performs a one-time
819
- runtime fallback for that session:
820
-
821
- 1. Switches provider from server-backed implementation to browser speech synthesis.
822
- 2. Rebinds highlight callbacks to the browser provider.
823
- 3. Retries the same `speak()` request once.
883
+ `showSingleSpeedOption: true` on the provider config to surface that one-option state.
824
884
 
825
- This keeps the inline/passage TTS controls usable during transient backend
826
- incidents without requiring host-side reconfiguration.
885
+ ### Browser Fallback
827
886
 
828
- Telemetry emitted for observability:
829
-
830
- - `pie-tool-runtime-fallback` (fallback switch succeeded)
831
- - `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.
832
892
 
833
893
  `provider.runtime.authFetcher` is optional. Add it only when your host environment
834
894
  requires runtime auth material for TTS requests:
@@ -838,7 +898,8 @@ tools: {
838
898
  providers: {
839
899
  textToSpeech: {
840
900
  enabled: true,
841
- backend: 'polly',
901
+ backend: 'server',
902
+ serverProvider: 'polly',
842
903
  apiEndpoint: '/api/tts',
843
904
  provider: {
844
905
  runtime: {
@@ -858,10 +919,10 @@ provider config: a returned `authToken` is sent as `Authorization: Bearer <token
858
919
  and returned `headers` with every synthesis request, and under
859
920
  `includeAuthOnAssetFetch` the `Authorization` header also reaches the custom
860
921
  transport's speech-mark and audio fetches. A failed fetch falls back to browser
861
- speech and reports `pie-tool-init-error`.
922
+ speech and reports `pie-tool-init-fallback`.
862
923
 
863
924
  For a server that authenticates by cookie on another origin, set `credentials:
864
- "include"` in the `textToSpeech` provider settings, beside
925
+ "include"` on the `textToSpeech` provider config, beside
865
926
  `includeAuthOnAssetFetch`. The cookie reaches speech-mark and audio fetches only
866
927
  for origins in `assetOrigins`; unset, every TTS fetch keeps the browser default.
867
928
 
@@ -900,27 +961,34 @@ SchoolCity is used as a host-configured integration example for custom transport
900
961
  Toolkit defaults still remain browser/standard providers unless the host explicitly
901
962
  configures custom server-backed TTS.
902
963
 
903
- ## Test Attempt Session Adapter (pie backend)
964
+ ## Test Attempt Session
904
965
 
905
- 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.
906
968
 
907
969
  ```typescript
908
970
  import {
909
- mapActivityToTestAttemptSession,
971
+ createNewTestAttemptSession,
910
972
  toItemSessionsRecord,
911
- buildActivitySessionPatchFromTestAttemptSession
973
+ upsertItemSessionFromPieSessionChange,
912
974
  } from "@pie-players/pie-assessment-toolkit";
913
975
 
914
- const testAttemptSession = mapActivityToTestAttemptSession({
915
- activityDefinition,
916
- 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,
917
988
  });
918
989
 
919
990
  // Use in section-player handoff (same item session shape as item players expect)
920
991
  const itemSessions = toItemSessionsRecord(testAttemptSession);
921
-
922
- // Host-owned backend persistence payload
923
- const patch = buildActivitySessionPatchFromTestAttemptSession(testAttemptSession);
924
992
  ```
925
993
 
926
994
  ### Integration Boundary
@@ -944,21 +1012,17 @@ The persistence strategy works with the same `SectionControllerSessionState` sha
944
1012
 
945
1013
  - **ToolkitCoordinator**: ⭐ NEW - Centralized service orchestration
946
1014
  - **ElementToolStateStore**: ⭐ NEW - Element-level ephemeral tool state management
947
- - **ToolRegistry**: ⭐ NEW - Registry-based tool management with QTI 3.0 PNP support
948
- - **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
949
1017
  - **ToolCoordinator**: Manages z-index layering and visibility for floating tools
950
1018
  - **HighlightCoordinator**: Separate highlight layers for TTS (temporary) and annotations (persistent)
951
1019
  - **TTSService**: Text-to-speech with QTI 3.0 catalog support
952
1020
  - **AccessibilityCatalogResolver**: QTI 3.0 accessibility catalog management
953
1021
  - **SSMLExtractor**: Automatic extraction of embedded `<speak>` tags
954
- - **ThemeProvider**: Consistent accessibility theming
955
1022
 
956
- ### ✅ QTI 3.0 Standard Access Features
1023
+ ### ✅ PNP Support Ids
957
1024
 
958
- - **95+ Standardized Features**: Complete QTI 3.0 / IMS AfA 3.0 accessibility features
959
- - **8 Feature Categories**: Visual, auditory, motor, cognitive, reading, navigation, linguistic, assessment
960
- - **Example Configurations**: Illustrative PNP profile examples (low vision, dyslexia, ADHD, etc.)
961
- - **Tool Mappings**: Every packaged tool registration maps 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.
962
1026
 
963
1027
  ### ✅ Section Player Integration
964
1028
 
@@ -990,7 +1054,8 @@ export interface ToolkitCoordinatorConfig {
990
1054
  providers?: {
991
1055
  textToSpeech?: {
992
1056
  enabled?: boolean;
993
- backend?: 'browser' | 'polly' | 'google' | 'server';
1057
+ backend?: 'browser' | 'server';
1058
+ serverProvider?: 'polly' | 'google' | 'custom';
994
1059
  defaultVoice?: string;
995
1060
  rate?: number;
996
1061
  };
@@ -1019,14 +1084,9 @@ export interface ToolkitCoordinatorConfig {
1019
1084
  ### Methods
1020
1085
 
1021
1086
  ```typescript
1022
- // Get all services as a bundle
1023
- const services = coordinator.getServiceBundle();
1024
- // Returns: { ttsService, toolCoordinator, highlightCoordinator, elementToolStateStore, catalogResolver }
1025
-
1026
1087
  // Tool configuration
1027
- coordinator.isToolEnabled('textToSpeech'); // Check if tool is enabled
1028
1088
  coordinator.getToolConfig('textToSpeech'); // Get tool-specific config
1029
- 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
1030
1090
 
1031
1091
  // Final teardown by the owner that constructed the coordinator
1032
1092
  await coordinator.dispose();
@@ -1138,6 +1198,8 @@ unsubscribe();
1138
1198
  ### TTSService
1139
1199
 
1140
1200
  ```typescript
1201
+ import { BrowserTTSProvider, TTSService } from '@pie-players/pie-assessment-toolkit';
1202
+
1141
1203
  const ttsService = new TTSService();
1142
1204
 
1143
1205
  // Initialize with provider
@@ -1146,8 +1208,8 @@ await ttsService.initialize(new BrowserTTSProvider());
1146
1208
  // Set catalog resolver for SSML support
1147
1209
  ttsService.setCatalogResolver(catalogResolver);
1148
1210
 
1149
- // Playback
1150
- 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, {
1151
1213
  catalogId: 'prompt-001',
1152
1214
  language: 'en-US'
1153
1215
  });
@@ -1166,36 +1228,46 @@ await ttsService.updateSettings({
1166
1228
 
1167
1229
  ### ToolCoordinator
1168
1230
 
1169
- ```typescript
1170
- const toolCoordinator = new ToolCoordinator();
1171
-
1172
- // Register tools
1173
- 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:
1174
1233
 
1175
- // Manage visibility
1176
- toolCoordinator.showTool('calculator');
1177
- toolCoordinator.hideTool('calculator');
1178
- toolCoordinator.toggleTool('calculator');
1234
+ ```typescript
1235
+ import {
1236
+ createToolCoordinatorRegistration,
1237
+ ZIndexLayer,
1238
+ } from '@pie-players/pie-assessment-toolkit/tools/registration';
1179
1239
 
1180
- // Z-index management
1181
- toolCoordinator.bringToFront(element);
1240
+ const registration = createToolCoordinatorRegistration('Calculator', ZIndexLayer.TOOL);
1182
1241
 
1183
- // Check state
1184
- 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());
1185
1250
  ```
1186
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
+
1187
1257
  ### HighlightCoordinator
1188
1258
 
1189
1259
  ```typescript
1260
+ import { HighlightColor, HighlightCoordinator } from '@pie-players/pie-assessment-toolkit';
1261
+
1190
1262
  const highlightCoordinator = new HighlightCoordinator();
1191
1263
 
1192
1264
  // TTS highlights (temporary)
1193
- highlightCoordinator.highlightTTSWord(textNode, start, end);
1265
+ highlightCoordinator.highlightTTSWord([wordRange]); // one range per tree the word spans
1194
1266
  highlightCoordinator.highlightTTSSentence([range1, range2]);
1195
1267
  highlightCoordinator.clearTTS();
1196
1268
 
1197
1269
  // Annotation highlights (persistent)
1198
- const id = highlightCoordinator.addAnnotation(range, 'yellow');
1270
+ const id = highlightCoordinator.addAnnotation(range, HighlightColor.YELLOW);
1199
1271
  highlightCoordinator.removeAnnotation(id);
1200
1272
  ```
1201
1273
 
@@ -1220,6 +1292,8 @@ maintaining, across every scheme it ships.
1220
1292
  ### AccessibilityCatalogResolver
1221
1293
 
1222
1294
  ```typescript
1295
+ import { AccessibilityCatalogResolver } from '@pie-players/pie-assessment-toolkit';
1296
+
1223
1297
  const resolver = new AccessibilityCatalogResolver(
1224
1298
  assessment.accessibilityCatalogs,
1225
1299
  'en-US'
@@ -1283,6 +1357,8 @@ such as TTS may still call `getAlternative(...)` with a context built by
1283
1357
  ### SSMLExtractor
1284
1358
 
1285
1359
  ```typescript
1360
+ import { SSMLExtractor } from '@pie-players/pie-assessment-toolkit';
1361
+
1286
1362
  const extractor = new SSMLExtractor();
1287
1363
 
1288
1364
  // Extract from item config
@@ -1313,20 +1389,22 @@ needs them — signing's card validators and its resolution rules live in
1313
1389
 
1314
1390
  ### Feature policy without a placement
1315
1391
 
1316
- `ToolPolicyEngine.decideFeature(featureId)` (and
1317
- `ToolkitCoordinator.decideFeaturePolicy(featureId)`) resolve one feature id
1392
+ `ToolPolicyEngine.decideFeature(featureId, scope?)` (and
1393
+ `ToolkitCoordinator.decideFeaturePolicy(featureId, scope?)`) resolve one feature id
1318
1394
  through `PnpPolicySource`'s six-level precedence, independent of any toolbar
1319
1395
  placement. Use it for capabilities that are not toolbar surfaces — signing is the
1320
1396
  first — where a placement-scoped `decide(...)` would answer the wrong question:
1321
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.
1322
1400
 
1323
- `createEmptyPersonalNeedsProfile()` is the only profile this package ships, and it
1324
- 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
1325
1402
  of the program rather than of a capability — TTS is a universal feature in one
1326
1403
  program and a documented accommodation in another — so it belongs in policy
1327
1404
  configuration alongside the district and test-administration levels. Hosts that
1328
1405
  want today's universal set take `createUniversalPersonalNeedsProfile()` from
1329
- `@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()`.
1330
1408
 
1331
1409
  Nothing derives a profile from the registry any more. Doing so read registry
1332
1410
  membership as eligibility tier — registration means "policy-addressable", not
@@ -1352,10 +1430,15 @@ The section player provides automatic ToolkitCoordinator integration:
1352
1430
 
1353
1431
  <script type="module">
1354
1432
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
1355
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
1433
+ import {
1434
+ createPackagedToolRegistry,
1435
+ DEFAULT_TOOL_MODULE_LOADERS,
1436
+ } from '@pie-players/pie-default-tool-loaders';
1356
1437
 
1357
1438
  // Create coordinator
1358
- const toolRegistry = createPackagedToolRegistry();
1439
+ const toolRegistry = createPackagedToolRegistry({
1440
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1441
+ });
1359
1442
  const coordinator = new ToolkitCoordinator({
1360
1443
  assessmentId: 'my-assessment',
1361
1444
  toolRegistry,
@@ -1378,21 +1461,27 @@ The section player provides automatic ToolkitCoordinator integration:
1378
1461
  // - Extracts services from coordinator
1379
1462
  // - Scopes its runtime engine to `section-id`
1380
1463
  // - Provides runtime context to child components
1381
- // - Manages SSML extraction
1464
+ // - Registers item catalogs, including SSML catalogs a host extracted
1465
+ // into `config.extractedCatalogs` with `SSMLExtractor`
1382
1466
  // - Handles catalog lifecycle
1383
1467
  </script>
1384
1468
  ```
1385
1469
 
1386
1470
  ### Runtime Context Contract
1387
1471
 
1388
- The toolkit now exports a shared context key used by section-player and toolkit
1389
- components:
1472
+ Section-player and toolkit components share one runtime context. An element
1473
+ inside the toolkit's tree connects to it:
1390
1474
 
1391
1475
  ```typescript
1392
1476
  import {
1393
- assessmentToolkitRuntimeContext,
1477
+ connectToolRuntimeContext,
1394
1478
  type AssessmentToolkitRuntimeContext
1395
1479
  } from "@pie-players/pie-assessment-toolkit";
1480
+
1481
+ let runtime: AssessmentToolkitRuntimeContext | undefined;
1482
+ const disconnect = connectToolRuntimeContext(hostElement, (value) => {
1483
+ runtime = value;
1484
+ });
1396
1485
  ```
1397
1486
 
1398
1487
  `AssessmentToolkitRuntimeContext` carries ambient orchestration dependencies
@@ -1422,7 +1511,7 @@ player.section = mySection;
1422
1511
 
1423
1512
  // Internally creates:
1424
1513
  // new ToolkitCoordinator({
1425
- // assessmentId: 'section-demo-direct', // or the assessment-id attribute
1514
+ // assessmentId: 'section-demo-direct', // or runtime.assessmentId
1426
1515
  // toolRegistry, // the player's toolRegistry, else the packaged registry
1427
1516
  // tools: player.runtime?.tools // no tools are placed when this is unset
1428
1517
  // })
@@ -1444,9 +1533,14 @@ import {
1444
1533
  createToolsConfig,
1445
1534
  ToolkitCoordinator
1446
1535
  } from "@pie-players/pie-assessment-toolkit";
1447
- 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";
1448
1540
 
1449
- const toolRegistry = createPackagedToolRegistry();
1541
+ const toolRegistry = createPackagedToolRegistry({
1542
+ toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1543
+ });
1450
1544
  const { config, diagnostics } = createToolsConfig({
1451
1545
  source: "host.bootstrap",
1452
1546
  strictness: "error",
@@ -1474,7 +1568,7 @@ const coordinator = new ToolkitCoordinator({
1474
1568
  Notes:
1475
1569
  - `providers.textToSpeech` is the canonical TTS provider key.
1476
1570
  - `providers.tts` is rejected by the validation contract.
1477
- - 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.
1478
1572
  - Hosts can react to framework errors via the `framework-error` DOM event,
1479
1573
  the `onFrameworkError(model)` callback prop, or by subscribing directly
1480
1574
  to the package-internal bus via
@@ -1488,133 +1582,63 @@ Notes:
1488
1582
 
1489
1583
  ## Section Runtime Engine (advanced)
1490
1584
 
1491
- The toolkit exposes a layered **section runtime engine** that consolidates
1492
- runtime resolution, FSM-driven stage progression, framework-error reporting,
1493
- DOM-event fan-out, and instrumentation into a single object hosts can mount
1494
- and dispose. The engine is what `<pie-section-player-…>` and
1495
- `<pie-assessment-toolkit>` use internally, and it is also the surface
1496
- custom hosts (or alternate layout shells) consume directly.
1497
-
1498
- ### Two import paths
1499
-
1500
- The engine ships with two deliberately separate entry points so consumers
1501
- pick the stability surface that matches their use case:
1502
-
1503
- - **Stable facade — `@pie-players/pie-assessment-toolkit/runtime/engine`.**
1504
- Narrow, semver-stable surface for hosts that want to mount, drive, and
1505
- dispose a section runtime. Re-exports `SectionRuntimeEngine`,
1506
- `SECTION_RUNTIME_ENGINE_KEY` (Svelte context), the cross-CE host
1507
- context (`sectionRuntimeEngineHostContext`), and the consumer-side
1508
- helper for that bridge (`connectSectionRuntimeEngineHostContext`).
1509
- The cross-CE host context exposes only a lifecycle handle; controller
1510
- methods stay on `SectionRuntimeEngine`.
1511
- - **Internal surface — `@pie-players/pie-assessment-toolkit/runtime/internal`.**
1512
- Wider, evolving surface for advanced hosts that need to construct an
1513
- engine manually, inspect FSM state, or build alternate fan-out paths.
1514
- Exposes `SectionEngineCore`, the four adapter bridges
1515
- (`createDomEventBridge`, `createFrameworkErrorBridge`,
1516
- `createCoordinatorBridge`, `createInstrumentationBridge`),
1517
- `FrameworkErrorBus`, cohort helpers,
1518
- and the `resolveRuntime` / `resolveToolsConfig` /
1519
- `resolveSectionEngineRuntimeState` helpers. Symbols here may change
1520
- between minor versions with a changeset note.
1521
-
1522
- ## Writing a capability package
1523
-
1524
- `@pie-players/pie-assessment-toolkit/tools/internal` is what a capability
1525
- package imports: the `ToolRegistration` contract, the `ToolProviderApi` its
1526
- provider descriptor creates, the surface and content dependency types,
1527
- `resolveToolTag` and the toolbar registration helpers. Same
1528
- stability contract as the other `*/internal` entry points — symbols may change
1529
- between minor versions with a changeset note.
1530
-
1531
- Import it rather than the package root: the root pulls in `ToolkitCoordinator`,
1532
- `TTSService` and the components, none of which a registration needs, and a
1533
- capability bundle that inlines them ends up with a second `ToolRegistry` class
1534
- that fails every `instanceof` across the host boundary. Mark the toolkit external
1535
- in the package's build with a pattern that covers subpaths, not a bare specifier.
1536
-
1537
- `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1538
- registration, a content resolver, its own custom element, and no edit to any
1539
- generic package. `packages/default-tool-loaders/README.md` covers how a
1540
- deployment then composes it in, and `docs/TOOL_REGISTRY.md` the registration and
1541
- host-surface contracts.
1542
-
1543
- ### Lifecycle emit coordination
1544
-
1545
- When `<pie-assessment-toolkit>` is nested inside a section-player layout,
1546
- the layout kernel publishes a lifecycle handle via
1547
- `sectionRuntimeEngineHostContext`. The toolkit detects that host
1548
- lifecycle owner and **suppresses its own external lifecycle DOM emits
1549
- and `onStageChange` callback** in favor of the layout CE host. From the
1550
- outside, one cohort yields one `pie-stage-change` /
1551
- `pie-loading-complete` chain on the layout CE host regardless of wrapper
1552
- depth. Controller-side
1553
- registration, content loading, session propagation, and persistence
1554
- remain toolkit-local through its own `SectionRuntimeEngine` instance.
1555
- A standalone `<pie-assessment-toolkit>` (no host context) emits from
1556
- its own engine.
1557
-
1558
- **Detection.** If a custom layout shell emits two `pie-stage-change`
1559
- events per stage transition (or two `pie-loading-complete` per cohort)
1560
- on the same layout CE — typically with two distinct `detail.runtimeId`
1561
- values — the shell has not published its engine via
1562
- `sectionRuntimeEngineHostContext`, so the wrapped
1563
- `<pie-assessment-toolkit>` falls back to its standalone lifecycle emit
1564
- 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.
1565
1615
 
1566
1616
  ### Common-host wiring example
1567
1617
 
1568
- Most hosts never construct the engine directly — the section-player
1569
- layout CE and the toolkit CE handle it. Use the facade only when
1570
- building an alternate layout shell (e.g. a custom kernel host). The
1571
- 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:
1572
1624
 
1573
1625
  ```ts
1574
- import { ContextProvider } from "@pie-players/pie-context";
1575
1626
  import {
1576
1627
  SectionRuntimeEngine,
1577
- sectionRuntimeEngineHostContext,
1578
- } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1579
- import {
1580
- FrameworkErrorBus,
1581
1628
  makeCohort,
1582
- } from "@pie-players/pie-assessment-toolkit/runtime/internal";
1629
+ } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1583
1630
 
1584
- const bus = new FrameworkErrorBus();
1585
1631
  const engine = new SectionRuntimeEngine();
1586
1632
 
1587
- // 1. Attach to the layout CE host. `sourceCe` is stamped onto every
1588
- // DOM event the engine dispatches and is required.
1589
- engine.attachHost({
1590
- host: layoutHostElement,
1591
- sourceCe: "my-custom-layout",
1592
- frameworkErrorBus: bus,
1593
- coordinator: toolkitCoordinator,
1594
- });
1633
+ // 1. Attach to the layout host. `sourceCe` is stamped onto every event.
1634
+ engine.attachHost({ host: layoutHostElement, sourceCe: "my-custom-layout" });
1595
1635
 
1596
- // 2. Publish a lifecycle handle on the layout CE host so any wrapped
1597
- // <pie-assessment-toolkit> suppresses duplicate external lifecycle
1598
- // emits. The toolkit still owns its controller registration/session
1599
- // plumbing locally.
1600
- const engineProvider = new ContextProvider(layoutHostElement, {
1601
- context: sectionRuntimeEngineHostContext,
1602
- initialValue: {
1603
- engine: {
1604
- getRuntimeId: () => engine.getRuntimeId(),
1605
- },
1606
- },
1636
+ // 2. (Optional) Subscribe to the output batches the DOM bridge dispatches.
1637
+ engine.subscribe((outputs) => {
1638
+ // `stage-change` and `loading-complete` outputs
1607
1639
  });
1608
- engineProvider.connect();
1609
1640
 
1610
- // 3. (Optional) Subscribe to the structured output stream — same set
1611
- // of outputs the DOM-event bridge fans out to the host element.
1612
- engine.subscribe((output) => {
1613
- // tap stage transitions, readiness updates, framework errors,
1614
- // instrumentation events
1615
- });
1616
-
1617
- // 4. Drive the engine. Use real `SectionEngineInput` shapes:
1641
+ // 3. Drive the engine with `SectionEngineInput`s.
1618
1642
  const cohort = makeCohort({ sectionId, attemptId });
1619
1643
  engine.dispatchInput({
1620
1644
  kind: "initialize",
@@ -1624,7 +1648,10 @@ engine.dispatchInput({
1624
1648
  itemCount,
1625
1649
  });
1626
1650
 
1627
- // 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:
1628
1655
  engine.dispatchInput({
1629
1656
  kind: "update-readiness-signals",
1630
1657
  signals: {
@@ -1633,34 +1660,44 @@ engine.dispatchInput({
1633
1660
  allLoadingComplete,
1634
1661
  runtimeError,
1635
1662
  },
1636
- loadedCount,
1637
1663
  itemCount,
1638
1664
  mode: "progressive",
1639
1665
  });
1640
1666
 
1641
- // On unmount:
1642
- engineProvider.disconnect();
1667
+ // On unmount; emits `disposed` for the active cohort.
1643
1668
  engine.dispose();
1644
1669
  ```
1645
1670
 
1646
- The DOM events `pie-stage-change`, `pie-loading-complete`, and
1647
- `framework-error` are dispatched on `host` automatically by the
1648
- adapter's `dom-event-bridge`. The canonical `onFrameworkError` callback
1649
- prop and the package-internal `FrameworkErrorBus` deliver each error
1650
- exactly once regardless of wrapper depth. The `framework-error` DOM
1651
- event on the layout CE host also delivers each error exactly once: the
1652
- section-player kernel intercepts the toolkit's bubbled emit at
1653
- `<pie-section-player-base>` and calls `event.stopPropagation()`, so the
1654
- layout host sees only the canonical engine-bridge emit. Direct
1655
- listeners on `<pie-assessment-toolkit>` itself still see the toolkit's
1656
- own emit (the toolkit dispatch reaches them before the kernel listener
1657
- runs). A coordinator the host passes in reports through the same
1658
- surfaces. `packages/section-player/tests/section-player-event-delivery.spec.ts`
1659
- pins these counts.
1660
- The layout host emits one `framework-error` DOM event per framework error.
1661
-
1662
- Hosts should listen to `pie-stage-change` (with the readiness detail also
1663
- 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.
1664
1701
 
1665
1702
  ## State Separation: Tool State vs Session Data
1666
1703
 
@@ -1725,8 +1762,7 @@ Full TypeScript definitions included:
1725
1762
  import type {
1726
1763
  ToolkitCoordinatorApi,
1727
1764
  ElementToolStateStoreApi,
1728
- ToolkitCoordinatorConfig,
1729
- ToolkitServiceBundle
1765
+ ToolkitCoordinatorConfig
1730
1766
  } from '@pie-players/pie-assessment-toolkit';
1731
1767
  ```
1732
1768
 
@@ -1760,7 +1796,7 @@ from tool configuration. Two sanitization layers apply:
1760
1796
 
1761
1797
  ## Related Documentation
1762
1798
 
1763
- - **[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
1764
1800
  - **[PNP Configuration Guide](docs/PNP_CONFIGURATION.md)** - ⭐ NEW - How to configure student profiles, district policies, and governance rules
1765
1801
  - [ToolkitCoordinator Architecture](../../docs/architecture/architecture.md#toolkitcoordinator-centralized-service-management) - Design decisions and patterns
1766
1802
  - [Section Player README](../section-player/README.md) - Section player integration