@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0

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 (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  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 +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  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/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  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 +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
package/README.md CHANGED
@@ -1,8 +1,36 @@
1
1
  # PIE Assessment Toolkit
2
2
 
3
- **Independent, composable services** for coordinating tools, accommodations, and item players in assessment applications.
4
-
5
- This is not an opinionated framework or monolithic "player" - it's a toolkit that solves specific problems through centralized service management.
3
+ The assessment toolkit coordinates the services around PIE item players: tool
4
+ policy and placement, tool providers, accommodations, text-to-speech (TTS),
5
+ highlighting and per-element tool state. It ships the `ToolkitCoordinator`, the
6
+ services it owns and the `<pie-assessment-toolkit>` custom element. This README
7
+ is for host integrators wiring the toolkit into a section player, an item player
8
+ or their own shell, and for contributors working on toolkit core. The
9
+ [tools and accommodations architecture](../../docs/tools-and-accomodations/architecture.md)
10
+ sets out the design, and [Configuring Tools](../../docs/tools-and-accomodations/tool_provider_system.md)
11
+ is the configuration guide.
12
+
13
+ ## Contents
14
+
15
+ - [Install](#install)
16
+ - [Capabilities](#capabilities)
17
+ - [Architecture overview](#architecture-overview)
18
+ - [Configuration: attributes and a constructed coordinator](#configuration-attributes-and-a-constructed-coordinator)
19
+ - [Quick Start](#quick-start)
20
+ - [Tool Configuration Model](#tool-configuration-model)
21
+ - [Test Attempt Session](#test-attempt-session)
22
+ - [ToolkitCoordinator API](#toolkitcoordinator-api)
23
+ - [ElementToolStateStore API](#elementtoolstatestore-api)
24
+ - [Service APIs](#service-apis)
25
+ - [Integration with Section Player](#integration-with-section-player)
26
+ - [Instrumentation and Observability](#instrumentation-and-observability)
27
+ - [Section Runtime Engine (advanced)](#section-runtime-engine-advanced)
28
+ - [Writing a capability package](#writing-a-capability-package)
29
+ - [State separation](#state-separation)
30
+ - [Examples](#examples)
31
+ - [TypeScript Support](#typescript-support)
32
+ - [Content trust boundary](#content-trust-boundary)
33
+ - [Related Documentation](#related-documentation)
6
34
 
7
35
  ## Install
8
36
 
@@ -15,144 +43,52 @@ The examples build their tool registry with `createPackagedToolRegistry()` from
15
43
  in its own dependencies. The section player depends on it too, but strict
16
44
  installers such as pnpm expose only the packages a host declares.
17
45
 
18
- Server-backed TTS (`backend: "polly"`, `"google"` or `"server"`) loads
46
+ Server-backed TTS (`backend: "server"`) loads
19
47
  `@pie-players/tts-client-server`, a dependency of
20
48
  `@pie-players/pie-default-tool-loaders` whose TTS registration imports it on
21
49
  first use. If it fails to load, TTS initialization reports a `provider-init`
22
50
  framework error and falls back to browser speech.
23
51
 
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
-
31
- ## What's New: ToolkitCoordinator
32
-
33
- ✨ **Centralized Service Management**: The new `ToolkitCoordinator` provides a single entry point for all toolkit services, simplifying initialization and configuration.
34
-
35
- **Before** (scattered services):
36
- ```typescript
37
- // Create 5+ services independently
38
- const ttsService = new TTSService();
39
- const toolCoordinator = new ToolCoordinator();
40
- const highlightCoordinator = new HighlightCoordinator();
41
- const catalogResolver = new AccessibilityCatalogResolver([...]);
42
- // Missing: ElementToolStateStore
43
-
44
- await ttsService.initialize(new BrowserTTSProvider());
45
- ttsService.setCatalogResolver(catalogResolver);
46
-
47
- // Pass all services separately
48
- player.ttsService = ttsService;
49
- player.toolCoordinator = toolCoordinator;
50
- // ...
51
- ```
52
-
53
- **After** (coordinator orchestrates):
54
- ```typescript
55
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
56
-
57
- // Create one coordinator with configuration
58
- const toolRegistry = createPackagedToolRegistry();
59
- const toolkitCoordinator = new ToolkitCoordinator({
60
- assessmentId: 'my-assessment',
61
- toolRegistry,
62
- tools: {
63
- providers: {
64
- textToSpeech: { enabled: true, backend: 'browser' },
65
- // Desmos needs an application key; see "Simple Default" below.
66
- calculator: { enabled: true }
67
- },
68
- placement: {
69
- section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
70
- item: ['calculator', 'textToSpeech', 'answerEliminator'],
71
- passage: ['textToSpeech']
72
- }
73
- }
74
- });
75
-
76
- // Pass single coordinator to section-player through runtime
77
- player.runtime = { ...(player.runtime ?? {}), coordinator: toolkitCoordinator };
78
- ```
79
-
80
- ## What Does It Solve?
81
-
82
- - **Centralized service management**: One coordinator owns all toolkit services
83
- - **Tool coordination**: z-index management, visibility state, element-level state
84
- - **Accommodation support**: IEP/504 tool configuration logic
85
- - **TTS + annotation coordination**: Prevent conflicts between highlights
86
- - **Event communication**: Standard contracts between components
87
- - **Accessibility theming**: Consistent high-contrast, font sizing
88
- - **State separation**: Ephemeral tool state separate from persistent session data
89
-
90
- ## Instrumentation and Observability
91
-
92
- Toolkit instrumentation is provider-agnostic and additive. It uses the shared
93
- `InstrumentationProvider` contract from `@pie-players/pie-players-shared`.
94
-
95
- ### Injection Path
96
-
97
- When toolkit is hosted by section/assessment player flows, the canonical
98
- provider path is the item-player loader config:
99
-
100
- - `runtime.player.loaderConfig.instrumentationProvider`
101
-
102
- ### Semantics
103
-
104
- - With `trackPageActions: true`, missing/`undefined` providers use the default New Relic provider path.
105
- - `instrumentationProvider: null` explicitly disables instrumentation.
106
- - Invalid provider objects are ignored (optional debug warning), also no-op.
107
- - Existing `item-player` behavior remains the compatibility anchor.
108
- - Debug overlays can consume the same stream by composing providers with
109
- `CompositeInstrumentationProvider` (for example New Relic + debug panel).
110
- - Toolkit telemetry forwarding uses the same provider path, so tool/backend
111
- instrumentation is sent to production providers and is visible in debug panel
112
- overlays.
113
-
114
- ### Toolkit-Owned Canonical Event Stream
115
-
116
- - `pie-toolkit-stage-change`
117
- - `pie-toolkit-runtime-owned`
118
- - `pie-toolkit-runtime-inherited`
119
- - `pie-toolkit-ready`
120
- - `pie-toolkit-section-ready`
121
- - `pie-toolkit-framework-error`
122
-
123
- Toolkit tool/backend operational stream:
52
+ ## Capabilities
124
53
 
125
- - `pie-tool-init-start|success|error`
126
- - `pie-tool-backend-call-start|success|error`
127
- - `pie-tool-library-load-start|success|error`
54
+ - **Centralized service management**: one coordinator owns the toolkit services
55
+ and hands them to the players
56
+ - **Tool coordination**: z-index management, visibility state and per-element
57
+ tool state
58
+ - **Accommodation support**: tool policy driven by the student's personal needs
59
+ profile (PNP), including tools an IEP (Individualized Education Program) or
60
+ Section 504 plan requires
61
+ - **TTS and annotation coordination**: TTS reading highlights and student
62
+ annotations share one highlight layer without conflict
63
+ - **Event communication**: typed events between players, toolkit and tools
64
+ - **Accessibility theming**: high-contrast color schemes through the
65
+ color-scheme tool (`theme`)
66
+ - **State separation**: tool state kept apart from PIE session data
128
67
 
129
- Ownership boundary: toolkit emits toolkit lifecycle semantics only. Section and
130
- assessment semantic streams stay in their own layers to avoid overlap. Bridge
131
- dedupe is a safety net, not a substitute for clear ownership.
68
+ ## Architecture overview
132
69
 
133
- ## Architecture Overview
134
-
135
- See the [ToolkitCoordinator section in the architecture overview](../../docs/architecture/architecture.md#toolkitcoordinator-centralized-service-management) for the current design documentation.
70
+ The [Assessment toolkit section of the architecture overview](../../docs/architecture/architecture.md#assessment-toolkit)
71
+ places the toolkit among the players.
136
72
 
137
73
  ### Core Principles
138
74
 
139
- 1. **Centralized Coordination**: ToolkitCoordinator orchestrates all services
75
+ 1. **Centralized Coordination**: ToolkitCoordinator owns the services, and players reach them through it
140
76
  2. **Composable Services**: Import only what you need (or use coordinator for convenience)
141
77
  3. **No Framework Lock-in**: Works with any JavaScript framework
142
- 4. **Product Control**: Products control navigation, persistence, layout, backend
78
+ 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 (`hooks.createSectionSessionPersistence` on the coordinator, `createAssessmentSessionPersistence` on the assessment player)
143
79
  5. **Standard Contracts**: Well-defined event types for component communication
144
- 6. **Element-Level Granularity**: Tool state tracked per PIE element, not per item
145
- 7. **State Separation**: Tool state (ephemeral) separate from PIE session data (persistent)
80
+ 6. **Element-Level Granularity**: Tool state is tracked per PIE element
81
+ 7. **State Separation**: Tool state is stored apart from PIE session data and never scored
146
82
 
147
- ## Configuration tiers: easy attribute + sophisticated `runtime`
83
+ ## Configuration: attributes and a constructed coordinator
148
84
 
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.
85
+ This package and `@pie-players/pie-section-player` give each input one entry
86
+ point (see [One tier per input](#one-tier-per-input)). A host that needs more
87
+ than the element inputs builds the coordinator itself.
152
88
 
153
- ### When to use each tier
89
+ ### Element inputs and a constructed coordinator
154
90
 
155
- - **Easy tier — top-level CE attributes / properties.** Use these for the
91
+ - **Element attributes / properties.** Use these for the
156
92
  common cases that are static for the lifetime of the player or that hosts
157
93
  want to set declaratively in HTML / templating frameworks. Example:
158
94
 
@@ -164,13 +100,14 @@ tier; the choice is about ergonomics, not capability.
164
100
  ></pie-assessment-toolkit>
165
101
  ```
166
102
 
167
- - **Sophisticated tier — passing a constructed `ToolkitCoordinator` (or a
168
- `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
103
+ - **A constructed `ToolkitCoordinator` (or a
104
+ `runtime` object on the player custom elements).** Use this for advanced cases: composed
105
+ configuration, dynamic overrides, runtime mutation, fields without an
106
+ attribute, or anything that benefits from being a single typed
171
107
  object passed by reference. Example:
172
108
 
173
109
  ```ts
110
+ import { ToolkitCoordinator } from "@pie-players/pie-assessment-toolkit";
174
111
  import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
175
112
 
176
113
  const toolRegistry = createPackagedToolRegistry();
@@ -192,73 +129,77 @@ Top-level attributes use kebab-case (`assessment-id`,
192
129
  `tool-config-strictness`). Section-player runtime configuration is grouped
193
130
  under the `runtime` object instead of duplicated as top-level props.
194
131
 
195
- ### Precedence rule
196
-
197
- The configuration object owns runtime fields. Section-player layout attributes
198
- cover identity, layout, diagnostics, and callback/event convenience:
132
+ ### One tier per input
199
133
 
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.
134
+ Each input has one entry point. On the section-player layout elements,
135
+ `runtime` carries `assessmentId`, the player, tool, accessibility,
136
+ coordinator, env and isolation fields, runtime factories and the `on*`
137
+ callbacks; attributes carry section identity, layout controls, `nds-icons`,
138
+ `locale` and `tool-config-strictness`. Unset inputs take their documented
139
+ defaults.
204
140
 
205
- ### Canonical tier-1 attribute set
206
-
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
- ```
141
+ `<pie-assessment-toolkit>` takes the same fields as its own properties. Its
142
+ string and boolean attributes are `assessment-id`, `section-id`, `attempt-id`,
143
+ `nds-icons`, `locale`, `content-language`, `lazy-init`,
144
+ `tool-config-strictness`, `enabled-tools`, `player-type` and `isolation`;
145
+ `section`, `env`, `tools` and `player` also accept JSON attributes. It reads
146
+ `pnpEnforcement` from `tools.pnpEnforcement`.
215
147
 
216
- Common members include:
148
+ ### Canonical attribute set
217
149
 
218
- - Identity: `assessment-id`, `section-id`, `attempt-id`
219
- - Runtime config on section-player CEs: `runtime`
220
- - Toolkit-only object properties: `tools`, `tool-registry`, `coordinator`,
150
+ - Identity: `assessment-id` on the toolkit, `section-id`, `attempt-id`
151
+ - Runtime config on section-player custom elements: `runtime`
152
+ - Toolkit-only object properties: `tools`, `toolRegistry`, `coordinator`,
221
153
  `accessibility`
222
- - Diagnostics: `tool-config-strictness`, `debug`. Framework-error
223
- delivery is via the canonical `onFrameworkError` callback prop and the
224
- `framework-error` DOM event dispatched on the layout CE host.
154
+ - Interface: `nds-icons`, `locale`
155
+ - Diagnostics: `tool-config-strictness`, and `debug` on the section-player layouts. Framework-error
156
+ delivery is via the `onFrameworkError` callback (a toolkit property,
157
+ `runtime.onFrameworkError` on a section player) and the
158
+ `framework-error` DOM event dispatched on the layout element.
225
159
 
226
- Documented exceptions to the mirror rule:
160
+ Inputs outside the runtime config:
227
161
 
228
162
  - Identity (`section-id`, `attempt-id`, `section`): per-attempt host
229
163
  state, not configuration.
230
- - Layout-only shell knobs on the section-player layout CEs
164
+ - Layout-only shell knobs on the section-player layout elements
231
165
  (`show-toolbar`, `toolbar-position`, `narrow-layout-breakpoint`,
232
- `split-pane-collapse-strategy`): layout-CE rendering concerns.
166
+ `split-pane-collapse-strategy`): rendering concerns of the layout element.
233
167
  - Per-region toolbar tool placement: hosts populate
234
168
  `tools.placement.item` / `tools.placement.passage` (object form) or
235
169
  `runtime.tools.placement.{item,passage}` directly.
236
- - Runtime-only keys on the section-player layout CEs
170
+ - Runtime-only keys on the section-player layout elements
237
171
  (`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.
172
+ `runtime.<key>`. Section-player layouts forward both to the wrapped
173
+ toolkit as properties. `<pie-assessment-toolkit>` keeps
174
+ `createSectionController` JS-only and takes `isolation` as an
175
+ attribute or a property. A `coordinator` passed to it wins over an
176
+ outer toolkit's.
245
177
 
246
- ### When to add a tier-1 attribute
178
+ ### Adding an attribute (contributors)
247
179
 
248
- Add a tier-1 attribute only if all of the following hold:
180
+ A new input becomes an attribute only if all of the following hold:
249
181
 
250
182
  - It is a common case that hosts set without composing a `ToolkitCoordinator`
251
183
  / `runtime` object.
252
184
  - Its value is a primitive or small typed object that round-trips through
253
185
  HTML attributes (string, boolean-like, number; structured data passes via
254
186
  property assignment).
255
- - It exists on every CE that conceptually owns the same knob, or has a
187
+ - It exists on every custom element that owns the same knob, or has a
256
188
  deliberate documented exclusion.
257
189
 
258
190
  Otherwise expose it through the configuration object only.
259
191
 
260
192
  ## Quick Start
261
193
 
194
+ Three defaults decide whether tools show up at all:
195
+
196
+ - Placement is empty by default. A tool appears only at the levels
197
+ `tools.placement` names, and a profile grant does not place a tool.
198
+ - The section player renders its section toolbar only when `show-toolbar` is
199
+ `true`; the attribute defaults to `false`.
200
+ - The default calculator, Desmos, needs an API key
201
+ ([Calculator providers](#calculator-providers)).
202
+
262
203
  ### Option 1: Use ToolkitCoordinator (Recommended)
263
204
 
264
205
  ```typescript
@@ -273,7 +214,7 @@ const coordinator = new ToolkitCoordinator({
273
214
  tools: {
274
215
  providers: {
275
216
  textToSpeech: { enabled: true, backend: 'browser' },
276
- // Desmos needs an application key; see "Simple Default" below.
217
+ // Desmos needs an API key; see "Calculator providers" below.
277
218
  calculator: { enabled: true }
278
219
  },
279
220
  placement: {
@@ -299,7 +240,7 @@ const toolState = coordinator.elementToolStateStore.getAllState();
299
240
 
300
241
  ### Controller Event Subscriptions (Helper First)
301
242
 
302
- For host-side session/progress logic, prefer helper subscriptions over the generic filter API. Subscriptions follow the toolkit's *active section cohort* automatically — a single `subscribe*` call survives navigation between sections without re-wiring:
243
+ For host-side session and progress logic, the helper subscriptions below are simpler than the generic filter API. A cohort is the `(sectionId, attemptId)` pair a section controller serves. Subscriptions follow the toolkit's active cohort, so a single `subscribe*` call survives navigation between sections without re-wiring:
303
244
 
304
245
  ```typescript
305
246
  const unsubscribeItem = coordinator.subscribeItemEvents({
@@ -320,10 +261,11 @@ unsubscribeItem?.();
320
261
  unsubscribeSection?.();
321
262
  ```
322
263
 
323
- Behavior pins (PIE-512 Phase D):
264
+ Subscription behavior:
324
265
 
325
266
  - Subscribe **after** the first `getOrCreateSectionController(...)` resolves; calling subscribe before any cohort exists throws.
326
267
  - 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.
268
+ - 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
269
  - Subscribing the **same listener function** twice replaces the first subscription (filter args from the second call win).
328
270
  - A listener that throws is caught and `console.warn`-logged; the throw does not interrupt fan-out to other listeners.
329
271
 
@@ -331,85 +273,12 @@ Use `subscribeSectionEvents(...)` when you need advanced/custom filtering mixes.
331
273
 
332
274
  To persist or snapshot an inactive section, use `coordinator.getSectionController({ sectionId, attemptId })` — that lookup is by id and is unaffected by the active-cohort behavior described above.
333
275
 
334
- #### Migrating from `<0.3.35` (BREAKING — pre-1.0)
335
-
336
- `0.3.35` is the first release where `subscribeSectionEvents` (and its two helper wrappers `subscribeItemEvents` / `subscribeSectionLifecycleEvents`) follows the toolkit's *active section cohort* automatically. The on-the-wire shape of the subscription args object changed.
337
-
338
- If your host code looked like this:
339
-
340
- ```typescript
341
- const unsub = coordinator.subscribeItemEvents({
342
- sectionId: 'section-1',
343
- attemptId: 'attempt-1',
344
- listener: handleEvent,
345
- });
346
- ```
347
-
348
- Update it to drop `sectionId` / `attemptId`:
349
-
350
- ```typescript
351
- const unsub = coordinator.subscribeItemEvents({
352
- listener: handleEvent,
353
- });
354
- ```
355
-
356
- What this means in practice for typed integrations:
357
-
358
- - **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
- - **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.
361
- - **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
- - **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
- - **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.
364
-
365
- If your local types were hand-rolled structural copies of the public arg types (e.g. an Angular wrapper duplicating the shape rather than importing the package types), `sectionId` / `attemptId` keys will compile but are dead code at runtime — recommend dropping them as part of the upgrade.
366
-
367
- #### Pre-Phase D vs Phase D wrapper pattern
368
-
369
- ```typescript
370
- // BEFORE (pre-Phase D): rebind for every section change because the
371
- // subscription was pinned to a sectionId.
372
- public handleToolkitReady(event: Event): void {
373
- const coordinator = (event as CustomEvent).detail?.coordinator;
374
- if (!coordinator) return;
375
- this.controllerUnsubscribe?.(); // detach prior pin
376
- const itemUnsub = coordinator.subscribeItemEvents({
377
- sectionId: this.sectionId,
378
- listener: handleItemEvent,
379
- });
380
- const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
381
- sectionId: this.sectionId,
382
- listener: handleSectionEvent,
383
- });
384
- this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
385
- }
386
- ```
387
-
388
- ```typescript
389
- // AFTER (Phase D): subscribe once; the listener follows the active
390
- // cohort across all subsequent navigation.
391
- public handleToolkitReady(event: Event): void {
392
- const coordinator = (event as CustomEvent).detail?.coordinator;
393
- if (!coordinator) return;
394
- this.toolkitCoordinator = coordinator;
395
- if (this.controllerUnsubscribe) return; // already subscribed; do nothing on re-fire
396
- const itemUnsub = coordinator.subscribeItemEvents({
397
- listener: handleItemEvent,
398
- });
399
- const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
400
- listener: handleSectionEvent,
401
- });
402
- this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
403
- }
404
- ```
405
-
406
276
  ### Option 2: Create Services Manually (Advanced)
407
277
 
408
278
  ```typescript
409
279
  import {
410
280
  TTSService,
411
281
  BrowserTTSProvider,
412
- ToolCoordinator,
413
282
  HighlightCoordinator,
414
283
  AccessibilityCatalogResolver,
415
284
  ElementToolStateStore
@@ -417,7 +286,6 @@ import {
417
286
 
418
287
  // Initialize each service independently
419
288
  const ttsService = new TTSService();
420
- const toolCoordinator = new ToolCoordinator();
421
289
  const highlightCoordinator = new HighlightCoordinator();
422
290
  const elementToolStateStore = new ElementToolStateStore();
423
291
  const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
@@ -425,92 +293,172 @@ const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
425
293
  await ttsService.initialize(new BrowserTTSProvider());
426
294
  ttsService.setCatalogResolver(catalogResolver);
427
295
 
428
- // Pass services individually
429
- player.ttsService = ttsService;
430
- player.toolCoordinator = toolCoordinator;
431
- // ...
296
+ // Use them from host-built UI
432
297
  ```
433
298
 
299
+ No player element takes services one by one. The section and assessment
300
+ players reach services only through a coordinator, `runtime.coordinator` on a
301
+ section player and `coordinator` on an assessment player; manually created
302
+ services serve host code that drives them directly. The tool coordinator is
303
+ not among them: it is created and disposed by its `ToolkitCoordinator`.
304
+
434
305
  ### Without a Section Player
435
306
 
436
- `<pie-assessment-toolkit>` needs no section. Bind none and it provides tools,
437
- policy and services to the item toolbars and item players inside it, which is
438
- how the toolkit accompanies a plain item player:
307
+ `<pie-assessment-toolkit>` needs no section. Bind none and it serves the item
308
+ toolbars inside it and the tools they open, around a plain item player that
309
+ takes nothing from the toolkit. `<pie-item-scope>` holds the item for its tools,
310
+ as it does in a section player's card. The tree this builds:
439
311
 
440
312
  ```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>
313
+ <pie-assessment-toolkit>
314
+ <pie-item-scope item-id="q1">
315
+ <pie-item-toolbar></pie-item-toolbar>
316
+ <div data-region="content">
317
+ <pie-item-player></pie-item-player>
318
+ </div>
319
+ </pie-item-scope>
444
320
  </pie-assessment-toolkit>
445
321
  ```
446
322
 
323
+ The host sets the toolkit's properties before the tree enters the document. A
324
+ scope registers as soon as it mounts, and that first registration starts the
325
+ coordinator from the toolkit's inputs at that moment; see the binding rules
326
+ below.
327
+
328
+ ![Item player with the toolkit: the host sets tools, toolRegistry, toolContextResolvers and assessment on a pie-assessment-toolkit and receives runtime-ready with the coordinator; a pie-item-scope per item registers the item and its settings with the coordinator, its item toolbar takes tool policy and services from the coordinator, and the tools act on the content region that holds the item player](../../docs/img/toolkit-item-scope.excalidraw.svg)
329
+
447
330
  ```typescript
448
331
  import '@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element';
332
+ import '@pie-players/pie-assessment-toolkit/components/item-scope-element';
449
333
  import '@pie-players/pie-assessment-toolkit/components/item-toolbar-element';
450
334
  import '@pie-players/pie-item-player';
451
- import {
452
- createPackagedToolRegistry,
453
- DEFAULT_TOOL_MODULE_LOADERS,
454
- } from '@pie-players/pie-default-tool-loaders';
335
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
455
336
 
456
337
  // With no section player, the registry's loaders are what define the tool elements.
457
- const toolRegistry = createPackagedToolRegistry({
458
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
459
- });
338
+ const toolRegistry = createPackagedToolRegistry();
460
339
 
461
- toolkit.tools = { placement: { item: ['calculator'] } };
340
+ const toolkit = document.createElement('pie-assessment-toolkit');
341
+ toolkit.tools = {
342
+ pnpEnforcement: 'on',
343
+ placement: { item: ['textToSpeech', 'calculator'] },
344
+ };
462
345
  toolkit.toolRegistry = toolRegistry;
463
346
  toolkit.toolContextResolvers = toolContextResolvers;
464
347
  toolkit.assessment = { id: 'a1', personalNeedsProfile: { supports: ['calculator'] } };
465
348
 
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
349
+ const scope = document.createElement('pie-item-scope');
350
+ scope.setAttribute('item-id', 'q1');
351
+ scope.item = item;
352
+ scope.innerHTML = `
353
+ <pie-item-toolbar></pie-item-toolbar>
354
+ <div data-region="content"><pie-item-player></pie-item-player></div>
355
+ `;
356
+
357
+ toolkit.append(scope);
358
+ container.append(toolkit);
469
359
  ```
470
360
 
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.
475
-
476
- 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.
485
-
486
- The toolkit builds its own coordinator at mount from `tools`, `enabled-tools`,
487
- `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
493
- coordinator reports feature policy asked with no assessment bound only while
494
- `pnp-enforcement` is `on`: a toolkit given no `assessment` and no enforcement
495
- has asked for no accommodation.
361
+ Inside a scope:
362
+
363
+ - The toolbars and tools take the item and its id from the scope, and act on
364
+ the scope or on the element its `scopeElement` property names.
365
+ `textToSpeech` reads the scope's `[data-region="content"]` first.
366
+ - A toolbar's own `item`, `item-id` and `scopeElement` override the scope, and
367
+ its `toolRegistry` overrides the toolkit's.
368
+ - The scope registers the item with the toolkit once it finds one above it, so
369
+ it may mount first. The toolkit files the item's accessibility catalogs,
370
+ which read-aloud speaks in place of the markup they name.
371
+ - Item player events pass through the scope unchanged, and the session stays
372
+ the item player's.
373
+ - A registry whose `toolModuleLoaders` leave a tool out renders a button for it
374
+ whose element never loads unless the host defines it.
375
+
376
+ **Profile changes.** A profile change is a new `assessment` value; the toolbars
377
+ re-derive on the policy change it emits. A host that holds the coordinator from
378
+ `runtime-ready`, or passes its own as `coordinator`, changes the profile with
379
+ `coordinator.updateAssessment(...)`; the toolkit applies its `assessment`
380
+ property only to a coordinator it owns. The
381
+ [`calculator-pnp` section demo](../../apps/section-demos/src/routes/%28demos%29/calculator-pnp/+page.svelte)
382
+ composes the owned-coordinator form.
383
+
384
+ **Readiness.** The toolkit announces `runtime-ready`, with
385
+ `{ runtimeId, coordinator, ownership }`, once per coordinator, with or without a
386
+ section. Without one, the coordinator starts at the first item that registers,
387
+ and a host reads its readiness from `coordinator.waitUntilReady()` or
388
+ `isReady()`. `toolkit-ready` and `section-ready` wait for a bound section, and
389
+ both fire again for every section the toolkit initializes, `toolkit-ready`
390
+ first, so a handler that needs the coordinator once guards itself. The toolkit
391
+ emits no stage events: `pie-stage-change` is the section player's.
392
+
393
+ **Coordinator binding.** The toolkit builds its own coordinator at mount from
394
+ `tools`, `enabled-tools`, `assessment-id`, `accessibility`, `lazy-init`,
395
+ `tool-config-strictness` and `toolRegistry`.
396
+
397
+ - Its first content binds it: the section, or without one the first item scope
398
+ that registers.
399
+ - Content that arrives after one of those inputs changed binds a coordinator
400
+ rebuilt from the current values, and the toolbars move to it.
401
+ - Once bound, a change to those inputs is reported once in the console and does
402
+ not reach the coordinator. The exception is a `toolRegistry` given to a
403
+ toolkit that had none, which the coordinator adopts in place.
404
+ - `tools.pnpEnforcement`, `assessment` and `toolContextResolvers` apply at any
405
+ time.
406
+ - The coordinator reports feature policy asked with no assessment bound only
407
+ while `tools.pnpEnforcement` is `on`: a toolkit given no `assessment` and no
408
+ enforcement has asked for no accommodation.
409
+
410
+ **Nested toolkits.** A toolkit nested in another inherits the outer one's
411
+ coordinator when the outer one has a coordinator by the time the inner one
412
+ connects, which holds for both mounted together. Otherwise the inner one builds
413
+ its own and keeps it, and reports an outer coordinator arriving later once in
414
+ the console. `isolation` `"force"` keeps a nested toolkit on its own coordinator
415
+ by design.
416
+
417
+ **Unmount.** At unmount the toolkit disposes the controller of every section it
418
+ bound. A coordinator it owns it then disposes too; one the host passes, or an
419
+ outer toolkit lends, stays with its owner, and a later toolkit on it restores
420
+ those sections from their sessions.
421
+
422
+ **TTS and provider startup.** Text-to-speech starts at the toolkit's first
423
+ content, once the section composes or the first item scope registers, and
424
+ `coordinator.waitUntilReady()` waits for it. With `lazy-init` it starts at the
425
+ first read-aloud instead, unless policy grants it. A tool provider or
426
+ text-to-speech that fails to start is a recoverable framework error: the tool
427
+ reports itself unavailable and the assessment goes on. When policy grants the
428
+ tool, through an item or district requirement, a profile support or a
429
+ test-administration override set to `true`, the failure is fatal, including one
430
+ that a later policy change grants. For a toolbar tool, only grants from
431
+ decisions that PNP enforcement applies to make the failure fatal.
496
432
 
497
433
  ## Tool Configuration Model
498
434
 
499
- The toolkit uses one canonical `tools` model with three concerns:
435
+ The toolkit uses one canonical `tools` model with four keys:
500
436
 
501
- - `policy`: allow/block constraints (global gates)
437
+ - `policy`: which tools may appear at all. A non-empty `allowed` list is an
438
+ allow-list; `blocked` removes a tool absolutely, whatever else grants it.
502
439
  - `placement`: where tools appear (`section`, `item`, `passage`). Each tool
503
440
  declares the levels it supports; `calculator` is item-only, and a tool placed
504
441
  at a level it does not support fails validation, which throws under the
505
442
  default `toolConfigStrictness: "error"`.
506
- - `providers`: provider/runtime options (calculator, textToSpeech, etc.)
443
+ - `providers`: provider and runtime options per tool (calculator,
444
+ textToSpeech and so on). `providers.<toolId>.enabled: false` vetoes the tool.
445
+ - `pnpEnforcement`: `"on"` or `"off"` forces personal needs profile gating;
446
+ omitted, the toolkit turns it on when the assessment carries profile or
447
+ district material ([PNP enforcement](docs/PNP_CONFIGURATION.md#pnp-enforcement)).
448
+
449
+ A profile, district policy or item setting names a tool by its `toolId`, and an
450
+ id no registered tool carries raises `tool-policy.unknownSupportId`.
451
+ [Support ids](docs/TOOL_REGISTRY.md#support-ids) lists the AfA PNP 3.0 terms the
452
+ packaged ids serve.
507
453
 
508
454
  Example:
509
455
 
510
456
  ```typescript
511
457
  tools: {
512
458
  policy: {
459
+ // Only these tools may appear...
513
460
  allowed: ['calculator', 'textToSpeech', 'answerEliminator', 'graph', 'periodicTable'],
461
+ // ...and graph never does.
514
462
  blocked: ['graph']
515
463
  },
516
464
  placement: {
@@ -529,25 +477,38 @@ tools: {
529
477
  }
530
478
  ```
531
479
 
532
- ### Scope and Lifecycle
480
+ With this configuration the section toolbar shows only `periodicTable`:
481
+ `graph` is blocked, and `protractor`, `lineReader` and `ruler` are outside the
482
+ allow-list. The item and passage placements show every tool they list.
533
483
 
534
- The runtime still distinguishes between contextual (`item`/`passage`) and section-wide tools:
484
+ ### Scope and Lifecycle
535
485
 
536
- Tool instances use structured IDs so scope is explicit:
486
+ Tool instances use structured ids that make their scope explicit:
537
487
 
538
488
  ```text
539
- <toolId>:<scopeLevel>:<scopeId>[:inline]
489
+ <toolId>:<scopeLevel>:<scopeId>
540
490
  ```
541
491
 
542
- Examples:
543
- - `calculator:section:section-1`
544
- - `calculator:item:item-42`
492
+ `scopeLevel` is one of `assessment`, `section`, `item`, `passage` or `rubric`,
493
+ and `createScopedToolId` throws on any other. The toolbars the players render
494
+ scope their ids at `section`, `item` or `passage`, and a rubric block's toolbar
495
+ scopes at `passage`. Examples from a section player:
496
+
497
+ - `lineReader:section:reading-1`
498
+ - `theme:section:reading-1`
499
+ - `calculator:item:q1`
545
500
  - `textToSpeech:passage:passage-1`
546
- - `highlighter:rubric:rubric-3`
501
+
502
+ The section player keeps every item card of a section mounted while the student
503
+ moves between its items. The ElementToolStateStore keeps answer eliminations,
504
+ annotation highlights and the color-scheme choice, and the coordinator's
505
+ `saveToolState`/`loadToolState` hooks carry them across reloads
506
+ ([Persistence Integration](#persistence-integration)). Which tools are open is
507
+ not restored, and graph points start over.
547
508
 
548
509
  ### Item-Level Tools (`tools.placement.item`)
549
510
 
550
- Tools that operate **within the context of a specific question/item**:
511
+ Tools that operate within the context of one item:
551
512
 
552
513
  ```typescript
553
514
  tools: {
@@ -558,24 +519,37 @@ tools: {
558
519
  ```
559
520
 
560
521
  **Characteristics:**
561
- - **Scope**: Bound to a specific item's DOM context
562
- - **Lifecycle**: Instance created/destroyed as you navigate between items
563
- - **State**: Isolated per-item (eliminations for Q5 don't affect Q6)
564
- - **UI Pattern**: Inline buttons in question headers/toolbars
565
- - **State Persistence**: Tracked per-item in ElementToolStateStore
566
-
567
- **Available Item-Level Tools:**
568
- - **TTS (Text-to-Speech)**: Reads the specific question/passage text
569
- - **Answer Eliminator**: Strikes through answer choices for that question
570
- - **Calculator**: Basic, scientific or graphing calculator
571
- - **Highlighter** (`annotationToolbar`): Highlights and annotates selected text; it opens from a text selection, outside the toolbars
522
+ - **Scope**: bound to one item's DOM context
523
+ - **Lifetime**: an item's tools live as long as its card, which stays mounted
524
+ until the section changes
525
+ - **State**: isolated per item (eliminations on Q5 don't affect Q6)
526
+ - **UI pattern**: buttons in the item toolbar; tools such as the calculator and
527
+ the dictionaries open in a floating panel
528
+ - **Persistence**: answer eliminations and annotation highlights are kept per
529
+ PIE element in the ElementToolStateStore
530
+
531
+ **Available item-level tools:**
532
+ - **Text-to-speech** (`textToSpeech`): reads the item or passage text
533
+ - **Answer eliminator** (`answerEliminator`, item only): strikes through answer
534
+ choices
535
+ - **Calculator** (`calculator`, item only): basic, scientific or graphing
536
+ calculator
537
+ - **Annotation toolbar** (`annotationToolbar`): highlights and annotates
538
+ selected text; it opens from a text selection, outside the toolbars
539
+ - **Line reader** (`lineReader`), **ruler** (`ruler`), **protractor**
540
+ (`protractor`), **graph** (`graph`) and **periodic table** (`periodicTable`)
541
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
542
+ `pictureDictionarySpanish`)
543
+
544
+ [Default Tools](docs/TOOL_REGISTRY.md#default-tools) lists every packaged tool
545
+ with the content it applies to.
572
546
 
573
547
  **Example Use Case:**
574
548
  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.
575
549
 
576
550
  ### Section-Level Tools (`tools.placement.section`)
577
551
 
578
- Tools that **float above the entire assessment** and persist across questions:
552
+ Tools that float above the section and stay open across its items:
579
553
 
580
554
  ```typescript
581
555
  tools: {
@@ -585,26 +559,30 @@ tools: {
585
559
  }
586
560
  ```
587
561
 
588
- The section player renders the section toolbar only when its `show-toolbar`
589
- attribute is `true`; the attribute defaults to `false`.
590
-
591
562
  **Characteristics:**
592
- - **Scope**: Section-wide, shared across all questions
593
- - **Lifecycle**: Single instance initialized for entire section
594
- - **State**: Persistent (a tool stays open and in place as you navigate)
595
- - **UI Pattern**: Draggable floating panels/overlays with z-index management
596
- - **State Persistence**: Global state maintained throughout section
597
-
598
- **Available Floating Tools:**
599
- - **Graph**: Coordinate plane
600
- - **Periodic Table**: Interactive periodic table reference
601
- - **Protractor**: Angle measurement tool
602
- - **Ruler**: Linear measurement tool (metric/imperial)
603
- - **Line Reader**: Reading guide/masking overlay
604
- - **Color Scheme** (`theme`): High-contrast color adjustments
563
+ - **Scope**: section-wide, shared across the section's items
564
+ - **Lifetime**: one instance per section; at a section change the tool
565
+ re-scopes to the new section and its state starts over
566
+ - **State**: an open tool stays open and in place while the student moves
567
+ between the section's items
568
+ - **UI pattern**: draggable floating panels with z-index management
569
+
570
+ **Available section-level tools:**
571
+ - **Graph** (`graph`): coordinate plane
572
+ - **Periodic table** (`periodicTable`): interactive periodic table reference
573
+ - **Protractor** (`protractor`): angle measurement
574
+ - **Ruler** (`ruler`): linear measurement (metric and imperial)
575
+ - **Line reader** (`lineReader`): reading guide and masking overlay
576
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
577
+ `pictureDictionarySpanish`)
578
+ - **The color-scheme tool** (`theme`): high-contrast color schemes; it also
579
+ supports assessment level
580
+
581
+ Every section-level tool except the color-scheme tool also supports item
582
+ placement.
605
583
 
606
584
  **Example Use Case:**
607
- 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.
585
+ A student opens the periodic table on Question 2 and moves it beside the passage. When they navigate to Question 7 of the same section, it is still open where they left it.
608
586
 
609
587
  ### When to Use Each
610
588
 
@@ -614,17 +592,20 @@ Use **item-level tools** when:
614
592
  - Tool appears inline with the question (space-efficient)
615
593
  - Tool behavior is contextual to the current item
616
594
 
617
- Use **floating tools** when:
595
+ Use **section-level tools** when:
618
596
  - Tool is a general-purpose utility used across multiple questions
619
- - State should persist across navigation
597
+ - State should persist while the student moves between a section's items
620
598
  - Tool needs independent positioning and sizing
621
599
  - Tool provides reference information or computation capability
622
600
 
623
- ### Configuration Example
601
+ ### Configuration example
624
602
 
625
- Complete example showing both types:
603
+ A coordinator shows a tool only where `tools.placement` lists it, and its
604
+ default placement is empty. This configuration places the commonly used tools
605
+ at both kinds of level:
626
606
 
627
607
  ```typescript
608
+ import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
628
609
  import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
629
610
 
630
611
  const toolRegistry = createPackagedToolRegistry();
@@ -661,44 +642,16 @@ const coordinator = new ToolkitCoordinator({
661
642
  });
662
643
  ```
663
644
 
664
- **Simple Default (All Tools Enabled):**
665
-
666
- For most use cases, simply enable all available tools:
645
+ ### Calculator providers
667
646
 
668
- ```typescript
669
- import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
670
-
671
- const toolRegistry = createPackagedToolRegistry();
672
- const coordinator = new ToolkitCoordinator({
673
- assessmentId: 'my-assessment',
674
- toolRegistry,
675
- tools: {
676
- placement: {
677
- section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
678
- item: ['calculator', 'textToSpeech', 'answerEliminator'],
679
- passage: ['textToSpeech']
680
- },
681
- providers: {
682
- calculator: {
683
- enabled: true,
684
- provider: {
685
- runtime: { authFetcher: fetchLicensedDesmosApiKey }
686
- }
687
- },
688
- textToSpeech: { enabled: true, backend: 'browser' }
689
- }
690
- }
691
- });
692
- ```
693
-
694
- The ToolkitCoordinator handles service initialization, provider management, and
695
- state coordination. With no calculator provider configuration, the Desmos
696
- implementation remains the default. Desmos's CDN rejects a `calculator.js`
697
- request without an `apiKey` (HTTP 403), so the Desmos calculator opens only when
698
- the host supplies `provider.init.apiKey` or `provider.runtime.authFetcher`, or
699
- has already loaded `window.Desmos`; the adapter's keyless request grants no
700
- Desmos license either. Runtime key delivery avoids committing the key but does
701
- not hide it from the browser's Desmos script request. The bundled open-source
647
+ With no calculator provider configured, the calculator is Desmos. Desmos's CDN
648
+ rejects a `calculator.js` request without an `apiKey` (HTTP 403), so the Desmos
649
+ calculator opens only when the host supplies a key through
650
+ `provider.runtime.authFetcher`, a `provider.init.proxyEndpoint` that returns
651
+ `{ apiKey }`, or `provider.init.apiKey`, or has already loaded `window.Desmos`.
652
+ Without one the adapter throws "An apiKey or proxyEndpoint is required to load
653
+ Desmos." Runtime key delivery keeps the key out of the bundle, and the
654
+ browser's Desmos script request still carries it. The bundled open-source
702
655
  implementation below needs no key.
703
656
 
704
657
  Select the separate GeoGebra implementation explicitly without changing the
@@ -736,7 +689,8 @@ tools: {
736
689
  providers: {
737
690
  textToSpeech: {
738
691
  enabled: true,
739
- backend: 'polly'
692
+ backend: 'server',
693
+ serverProvider: 'polly'
740
694
  }
741
695
  }
742
696
  }
@@ -745,14 +699,15 @@ tools: {
745
699
  By default, server-backed TTS resolves:
746
700
 
747
701
  - `apiEndpoint: '/api/tts'`
748
- - `transportMode: 'pie'`
749
- - `endpointValidationMode: 'voices'`
702
+ - `transportMode: 'custom'` for `serverProvider: 'custom'`, else `'pie'`
703
+ - `endpointValidationMode: 'voices'`, so the provider reads the voices route
704
+ before it reports ready; `'none'` skips the probe
750
705
 
751
706
  You can still set `apiEndpoint` explicitly when your host route is not `/api/tts`.
752
707
 
753
708
  ### Inline TTS Speed Options
754
709
 
755
- Inline TTS speed buttons are configurable via `speedOptions` in provider settings.
710
+ Inline TTS speed buttons are configurable via `speedOptions` on the provider config.
756
711
 
757
712
  ```typescript
758
713
  tools: {
@@ -760,9 +715,7 @@ tools: {
760
715
  textToSpeech: {
761
716
  enabled: true,
762
717
  backend: "browser",
763
- settings: {
764
- speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
765
- }
718
+ speedOptions: [2, 1.25, 1.5] // host options keep this order; Normal is added if omitted
766
719
  }
767
720
  }
768
721
  }
@@ -779,13 +732,11 @@ tools: {
779
732
  enabled: true,
780
733
  backend: "server",
781
734
  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
- }
735
+ speedOptions: [
736
+ { rate: 0.8, label: "Slow", ariaLabel: "Slow speed" },
737
+ { rate: 1, label: "Normal", ariaLabel: "Normal speed", default: true },
738
+ { rate: 1.5, label: "Fast", ariaLabel: "Fast speed" }
739
+ ]
789
740
  }
790
741
  }
791
742
  }
@@ -810,25 +761,15 @@ tools: {
810
761
  - Speed choices are exposed as a `Playback speed` radio group: one option is
811
762
  always selected, and choosing the already-selected speed is a no-op.
812
763
  - 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:
764
+ `showSingleSpeedOption: true` on the provider config to surface that one-option state.
820
765
 
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.
766
+ ### Browser Fallback
824
767
 
825
- This keeps the inline/passage TTS controls usable during transient backend
826
- incidents without requiring host-side reconfiguration.
827
-
828
- Telemetry emitted for observability:
829
-
830
- - `pie-tool-runtime-fallback` (fallback switch succeeded)
831
- - `pie-tool-runtime-fallback-error` (fallback switch failed)
768
+ The coordinator owns fallback to browser speech. When a server-backed provider
769
+ fails to initialize, the coordinator re-initializes TTS on the browser backend
770
+ and reports `pie-tool-init-fallback`. `TTSService.initialize` itself rejects
771
+ when its provider fails to start, and a playback failure after initialization
772
+ rejects that read without switching provider.
832
773
 
833
774
  `provider.runtime.authFetcher` is optional. Add it only when your host environment
834
775
  requires runtime auth material for TTS requests:
@@ -838,7 +779,8 @@ tools: {
838
779
  providers: {
839
780
  textToSpeech: {
840
781
  enabled: true,
841
- backend: 'polly',
782
+ backend: 'server',
783
+ serverProvider: 'polly',
842
784
  apiEndpoint: '/api/tts',
843
785
  provider: {
844
786
  runtime: {
@@ -858,10 +800,10 @@ provider config: a returned `authToken` is sent as `Authorization: Bearer <token
858
800
  and returned `headers` with every synthesis request, and under
859
801
  `includeAuthOnAssetFetch` the `Authorization` header also reaches the custom
860
802
  transport's speech-mark and audio fetches. A failed fetch falls back to browser
861
- speech and reports `pie-tool-init-error`.
803
+ speech and reports `pie-tool-init-fallback`.
862
804
 
863
805
  For a server that authenticates by cookie on another origin, set `credentials:
864
- "include"` in the `textToSpeech` provider settings, beside
806
+ "include"` on the `textToSpeech` provider config, beside
865
807
  `includeAuthOnAssetFetch`. The cookie reaches speech-mark and audio fetches only
866
808
  for origins in `assetOrigins`; unset, every TTS fetch keeps the browser default.
867
809
 
@@ -896,37 +838,44 @@ Recommended host boundary:
896
838
  upstream.
897
839
  - Browser never receives shared secret, API key, or signing material.
898
840
 
899
- SchoolCity is used as a host-configured integration example for custom transport.
900
- Toolkit defaults still remain browser/standard providers unless the host explicitly
901
- configures custom server-backed TTS.
841
+ The section demos' `/api/tts/sc` route, backed by the SC adapter
842
+ (`@pie-players/tts-server-sc`), is the worked example of custom transport. TTS
843
+ stays on browser speech unless the host configures a server backend.
902
844
 
903
- ## Test Attempt Session Adapter (pie backend)
845
+ ## Test Attempt Session
904
846
 
905
- The toolkit exposes a canonical `TestAttemptSession` runtime and a deterministic adapter for pie backend activity payloads from `../../kds/pie-api-aws`.
847
+ The toolkit exposes a canonical `TestAttemptSession` runtime. The host maps its
848
+ backend's attempt payload into it and back out.
906
849
 
907
850
  ```typescript
908
851
  import {
909
- mapActivityToTestAttemptSession,
852
+ createNewTestAttemptSession,
910
853
  toItemSessionsRecord,
911
- buildActivitySessionPatchFromTestAttemptSession
854
+ upsertItemSessionFromPieSessionChange,
912
855
  } from "@pie-players/pie-assessment-toolkit";
913
856
 
914
- const testAttemptSession = mapActivityToTestAttemptSession({
915
- activityDefinition,
916
- activitySession
857
+ let testAttemptSession = createNewTestAttemptSession({
858
+ testAttemptSessionIdentifier: attempt.id,
859
+ assessmentId: assessment.id,
860
+ seed: attempt.id,
861
+ itemIdentifiers: assessment.itemIdentifiers,
862
+ });
863
+
864
+ // Record a PIE session change against its item
865
+ testAttemptSession = upsertItemSessionFromPieSessionChange(testAttemptSession, {
866
+ itemIdentifier: "q1",
867
+ pieSessionId: change.session.id,
868
+ session: change.session,
917
869
  });
918
870
 
919
871
  // Use in section-player handoff (same item session shape as item players expect)
920
872
  const itemSessions = toItemSessionsRecord(testAttemptSession);
921
-
922
- // Host-owned backend persistence payload
923
- const patch = buildActivitySessionPatchFromTestAttemptSession(testAttemptSession);
924
873
  ```
925
874
 
926
875
  ### Integration Boundary
927
876
 
928
877
  - `@pie-players/pie-section-player` stays backend-agnostic and emits session/state changes.
929
- - Host applications own backend I/O to pie backend (`../../kds/pie-api-aws`).
878
+ - Host applications own backend I/O.
930
879
  - Hosts decide persistence policy (immediate, debounced, checkpoint, submit).
931
880
 
932
881
  ### Section session API (controller + persistence)
@@ -938,38 +887,6 @@ For section-level session flows, the toolkit supports two complementary APIs:
938
887
 
939
888
  The persistence strategy works with the same `SectionControllerSessionState` shape exposed by the controller, so hosts can choose bulk restore (`applySession`) and fine-grained updates (`updateItemSession`) without internal runtime coupling.
940
889
 
941
- ## Implementation Status
942
-
943
- ### ✅ Toolkit Services
944
-
945
- - **ToolkitCoordinator**: ⭐ NEW - Centralized service orchestration
946
- - **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
949
- - **ToolCoordinator**: Manages z-index layering and visibility for floating tools
950
- - **HighlightCoordinator**: Separate highlight layers for TTS (temporary) and annotations (persistent)
951
- - **TTSService**: Text-to-speech with QTI 3.0 catalog support
952
- - **AccessibilityCatalogResolver**: QTI 3.0 accessibility catalog management
953
- - **SSMLExtractor**: Automatic extraction of embedded `<speak>` tags
954
- - **ThemeProvider**: Consistent accessibility theming
955
-
956
- ### ✅ QTI 3.0 Standard Access Features
957
-
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
962
-
963
- ### ✅ Section Player Integration
964
-
965
- The toolkit integrates seamlessly with the **PIE Section Player**:
966
-
967
- - **Primary Interface**: Section player is the main integration point
968
- - **Default Coordinator**: Creates ToolkitCoordinator automatically if not provided
969
- - **Automatic SSML Extraction**: Extracts embedded `<speak>` tags from passages and items
970
- - **Catalog Lifecycle**: Manages item-level catalogs automatically
971
- - **Service Coordination**: All toolkit services work together automatically
972
-
973
890
  ## ToolkitCoordinator API
974
891
 
975
892
  ### Configuration
@@ -987,12 +904,18 @@ export interface ToolkitCoordinatorConfig {
987
904
  item?: string[];
988
905
  passage?: string[];
989
906
  };
907
+ pnpEnforcement?: 'on' | 'off'; // omitted: auto-detected from the assessment
990
908
  providers?: {
909
+ // Abridged; TextToSpeechToolProviderConfig carries every option.
991
910
  textToSpeech?: {
992
911
  enabled?: boolean;
993
- backend?: 'browser' | 'polly' | 'google' | 'server';
912
+ backend?: 'browser' | 'server';
913
+ serverProvider?: 'polly' | 'google' | 'custom';
914
+ apiEndpoint?: string;
994
915
  defaultVoice?: string;
995
916
  rate?: number;
917
+ speedOptions?: TTSSpeedOption[];
918
+ layoutMode?: 'reserved-row' | 'expanding-row' | 'floating-overlay' | 'left-aligned';
996
919
  };
997
920
  calculator?: {
998
921
  enabled?: boolean;
@@ -1008,68 +931,108 @@ export interface ToolkitCoordinatorConfig {
1008
931
  };
1009
932
  };
1010
933
  };
934
+ toolConfigStrictness?: 'off' | 'warn' | 'error'; // default 'error'
1011
935
  toolRegistry?: ToolRegistry | null;
936
+ toolContextResolvers?: ToolContextResolverMap;
1012
937
  accessibility?: {
1013
938
  catalogs?: any[];
1014
939
  language?: string;
1015
940
  };
941
+ hooks?: ToolkitCoordinatorHooks;
942
+ lazyInit?: boolean; // default false
943
+ eagerInit?: boolean; // default !lazyInit
1016
944
  }
1017
945
  ```
1018
946
 
947
+ A `ToolkitCoordinator` registers tool providers only from its `toolRegistry`.
948
+ Built without one, it adopts the registry of the toolkit it is bound to, such as
949
+ the section player's, which then validates its config and registers its
950
+ providers. A registry passed at construction is never replaced. Bound to a
951
+ toolkit that has none, it registers no providers, skips tool-id and placement
952
+ validation, and warns once (`tools.registryUnavailable`).
953
+
1019
954
  ### Methods
1020
955
 
1021
956
  ```typescript
1022
- // Get all services as a bundle
1023
- const services = coordinator.getServiceBundle();
1024
- // Returns: { ttsService, toolCoordinator, highlightCoordinator, elementToolStateStore, catalogResolver }
1025
-
1026
957
  // Tool configuration
1027
- coordinator.isToolEnabled('textToSpeech'); // Check if tool is enabled
1028
- coordinator.getToolConfig('textToSpeech'); // Get tool-specific config
1029
- coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // Update tool config
1030
-
1031
- // Final teardown by the owner that constructed the coordinator
1032
- await coordinator.dispose();
958
+ coordinator.getToolConfig('textToSpeech');
959
+ coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // replaces the tool's provider, which starts again on next use
960
+ coordinator.updateToolsPlacement({ item: ['calculator', 'textToSpeech'] });
961
+
962
+ // Tool policy
963
+ coordinator.decideToolPolicy({ level: 'item', scope: { level: 'item', scopeId: 'item-42' } });
964
+ coordinator.decideFeaturePolicy('transcript', { level: 'item', scopeId: 'item-42' });
965
+ const unregisterItem = coordinator.registerItemSettings('item-42', itemSettings);
966
+ const offPolicy = coordinator.onPolicyChange(listener);
967
+ const offDiagnostics = coordinator.onPolicyDiagnostic(listener);
968
+ coordinator.updateAssessment(assessment); // new profile or settings; null unbinds
969
+ coordinator.setPnpEnforcement('on'); // 'on' | 'off'; null returns to auto-detection
970
+ coordinator.getPolicyInputs(); // read-only inputs behind current decisions
971
+ const unregisterSource = coordinator.registerPolicySource(source);
972
+
973
+ // Tool requests: ask the toolbar hosting a tool to open it
974
+ const unregisterTarget = coordinator.registerToolRequestTarget(target);
975
+ coordinator.canRequestTool('calculator'); // false for a tool the deployment lacks
976
+ coordinator.requestTool({ toolId: 'calculator' }); // whether a toolbar claimed it; throws on an unknown id
977
+ const offTargets = coordinator.onToolRequestTargetsChange(listener);
978
+
979
+ // Providers and events
980
+ const provider = await coordinator.ensureProviderReady('textToSpeech');
981
+ const offErrors = coordinator.subscribeFrameworkErrors(listener);
982
+ const offCatalogs = coordinator.onCatalogsChange(listener);
983
+
984
+ // Readiness and teardown
985
+ await coordinator.waitUntilReady();
986
+ coordinator.isReady();
987
+ coordinator.getToolRegistry();
988
+ await coordinator.dispose(); // final teardown by the owner that constructed the coordinator
1033
989
  ```
1034
990
 
991
+ Each `register*`, `on*` and `subscribe*` call returns a function that undoes it.
992
+ The section-controller methods are under
993
+ [Controller Event Subscriptions](#controller-event-subscriptions-helper-first).
994
+
1035
995
  ### Direct Service Access
1036
996
 
1037
997
  All services are public properties for direct access:
1038
998
 
1039
999
  ```typescript
1040
1000
  coordinator.ttsService // TTSService instance
1041
- coordinator.toolCoordinator // ToolCoordinator instance
1001
+ coordinator.toolCoordinator // ToolCoordinatorApi, owned by the coordinator
1042
1002
  coordinator.highlightCoordinator // HighlightCoordinator instance
1043
1003
  coordinator.elementToolStateStore // ElementToolStateStore instance
1044
1004
  coordinator.catalogResolver // AccessibilityCatalogResolver instance
1005
+ coordinator.toolProviderRegistry // ToolProviderRegistry, the providers registered from toolRegistry
1045
1006
  ```
1046
1007
 
1047
1008
  ## ElementToolStateStore API
1048
1009
 
1049
- The `ElementToolStateStore` manages ephemeral tool state at the element level using globally unique composite keys.
1010
+ The `ElementToolStateStore` holds tool state per PIE element under globally unique composite keys. It is kept apart from PIE session data and leaves the browser only through the coordinator's `saveToolState` hook ([Persistence Integration](#persistence-integration)).
1050
1011
 
1051
1012
  ### Key Concepts
1052
1013
 
1053
- - **Global Element ID**: Composite key format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`
1054
- - **Element-Level Granularity**: State tracked per PIE element (not per item)
1055
- - **Ephemeral State**: Tool state is client-only, separate from PIE session data
1014
+ - **Global Element ID**: Composite key format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`, with `attemptId` `""` when the host names no attempt
1015
+ - **Element-Level Granularity**: State tracked per PIE element; a tool keyed by item or section leaves `elementId`, and for a section `itemId`, as `""`
1016
+ - **Per-Attempt State**: two attempts at one section never share a key
1017
+ - **Separate from session data**: tool state never enters the PIE session and is never scored
1056
1018
  - **Cross-Section Persistence**: State persists when navigating between sections
1057
1019
 
1058
1020
  ### ID Utilities
1059
1021
 
1060
1022
  ```typescript
1061
1023
  // Generate global element ID
1062
- const globalElementId = store.getGlobalElementId(
1063
- 'demo-assessment',
1064
- 'section-1',
1065
- 'question-1',
1066
- 'mc1'
1067
- );
1068
- // Returns: "demo-assessment:section-1:question-1:mc1"
1024
+ const globalElementId = store.getGlobalElementId({
1025
+ assessmentId: 'demo-assessment',
1026
+ sectionId: 'section-1',
1027
+ attemptId: 'attempt-1',
1028
+ itemId: 'question-1',
1029
+ elementId: 'mc1'
1030
+ });
1031
+ // Returns: "demo-assessment:section-1:attempt-1:question-1:mc1"
1069
1032
 
1070
1033
  // Parse global element ID
1071
1034
  const components = store.parseGlobalElementId(globalElementId);
1072
- // Returns: { assessmentId, sectionId, itemId, elementId }
1035
+ // Returns: { assessmentId, sectionId, attemptId, itemId, elementId }
1073
1036
  ```
1074
1037
 
1075
1038
  ### CRUD Operations
@@ -1099,7 +1062,7 @@ store.clearElement(globalElementId);
1099
1062
  // Clear state for a specific tool across all elements
1100
1063
  store.clearTool('answerEliminator');
1101
1064
 
1102
- // Clear all elements in a specific section
1065
+ // Clear all elements in a specific section, across its attempts
1103
1066
  store.clearSection('demo-assessment', 'section-1');
1104
1067
 
1105
1068
  // Clear all state
@@ -1108,17 +1071,21 @@ store.clearAll();
1108
1071
 
1109
1072
  ### Persistence Integration
1110
1073
 
1074
+ A coordinator's store reports changes to the coordinator, which forwards them
1075
+ to `hooks.saveToolState`; `loadToolState` is read once while the coordinator
1076
+ gets ready. Persist tool state through those hooks. `store.setOnStateChange`
1077
+ holds a single callback, so calling it on a coordinator's store disconnects
1078
+ `saveToolState`.
1079
+
1111
1080
  ```typescript
1112
- // Set callback for persistence (e.g., localStorage)
1113
- store.setOnStateChange((state) => {
1114
- localStorage.setItem('tool-state', JSON.stringify(state));
1081
+ const coordinator = new ToolkitCoordinator({
1082
+ assessmentId: 'demo-assessment',
1083
+ toolRegistry,
1084
+ hooks: {
1085
+ loadToolState: () => JSON.parse(localStorage.getItem('tool-state') ?? 'null'),
1086
+ saveToolState: (state) => localStorage.setItem('tool-state', JSON.stringify(state)),
1087
+ },
1115
1088
  });
1116
-
1117
- // Load state from persistence
1118
- const saved = localStorage.getItem('tool-state');
1119
- if (saved) {
1120
- store.loadState(JSON.parse(saved));
1121
- }
1122
1089
  ```
1123
1090
 
1124
1091
  ### Reactivity
@@ -1138,6 +1105,8 @@ unsubscribe();
1138
1105
  ### TTSService
1139
1106
 
1140
1107
  ```typescript
1108
+ import { BrowserTTSProvider, TTSService } from '@pie-players/pie-assessment-toolkit';
1109
+
1141
1110
  const ttsService = new TTSService();
1142
1111
 
1143
1112
  // Initialize with provider
@@ -1146,8 +1115,8 @@ await ttsService.initialize(new BrowserTTSProvider());
1146
1115
  // Set catalog resolver for SSML support
1147
1116
  ttsService.setCatalogResolver(catalogResolver);
1148
1117
 
1149
- // Playback
1150
- await ttsService.speak('Read this text', {
1118
+ // Playback: reads an element or a range, or the named card in its place
1119
+ await ttsService.speak(promptElement, {
1151
1120
  catalogId: 'prompt-001',
1152
1121
  language: 'en-US'
1153
1122
  });
@@ -1166,36 +1135,47 @@ await ttsService.updateSettings({
1166
1135
 
1167
1136
  ### ToolCoordinator
1168
1137
 
1169
- ```typescript
1170
- const toolCoordinator = new ToolCoordinator();
1171
-
1172
- // Register tools
1173
- toolCoordinator.registerTool('calculator', 'Calculator', element);
1138
+ The toolkit owns the coordinator. A tool element registers with the one in its
1139
+ runtime context and hands it the element once it renders:
1174
1140
 
1175
- // Manage visibility
1176
- toolCoordinator.showTool('calculator');
1177
- toolCoordinator.hideTool('calculator');
1178
- toolCoordinator.toggleTool('calculator');
1141
+ ```typescript
1142
+ import {
1143
+ createToolCoordinatorRegistration,
1144
+ ZIndexLayer,
1145
+ } from '@pie-players/pie-assessment-toolkit/tools/registration';
1179
1146
 
1180
- // Z-index management
1181
- toolCoordinator.bringToFront(element);
1147
+ const registration = createToolCoordinatorRegistration('Calculator', ZIndexLayer.TOOL);
1182
1148
 
1183
- // Check state
1184
- const isVisible = toolCoordinator.isToolVisible('calculator');
1149
+ // Re-registers when a republished context brings a new coordinator.
1150
+ $effect(() => registration.sync(coordinator, toolId));
1151
+ $effect(() => {
1152
+ if (coordinator && containerEl && toolId) {
1153
+ coordinator.updateToolElement(toolId, containerEl);
1154
+ }
1155
+ });
1156
+ onDestroy(() => registration.release());
1185
1157
  ```
1186
1158
 
1159
+ Visibility and stacking then go through the coordinator by scoped tool id:
1160
+ `showTool`, `hideTool`, `toggleTool`, `isToolVisible`, `bringToFront(element)`,
1161
+ and `getVisibleTools({ baseId })` / `hideAllTools({ baseId })` for one tool
1162
+ across scopes. Disposing the toolkit coordinator releases every entry. Its
1163
+ debug lines print only under `window.PIE_DEBUG = true`.
1164
+
1187
1165
  ### HighlightCoordinator
1188
1166
 
1189
1167
  ```typescript
1168
+ import { HighlightColor, HighlightCoordinator } from '@pie-players/pie-assessment-toolkit';
1169
+
1190
1170
  const highlightCoordinator = new HighlightCoordinator();
1191
1171
 
1192
1172
  // TTS highlights (temporary)
1193
- highlightCoordinator.highlightTTSWord(textNode, start, end);
1173
+ highlightCoordinator.highlightTTSWord([wordRange]); // one range per tree the word spans
1194
1174
  highlightCoordinator.highlightTTSSentence([range1, range2]);
1195
1175
  highlightCoordinator.clearTTS();
1196
1176
 
1197
1177
  // Annotation highlights (persistent)
1198
- const id = highlightCoordinator.addAnnotation(range, 'yellow');
1178
+ const id = highlightCoordinator.addAnnotation(range, HighlightColor.YELLOW);
1199
1179
  highlightCoordinator.removeAnnotation(id);
1200
1180
  ```
1201
1181
 
@@ -1210,16 +1190,20 @@ Reading highlights resolve through five `component-public` tokens registered in
1210
1190
  | `--pie-tts-word-underline` | `--pie-text` at 70% | Underline marking the current word |
1211
1191
  | `--pie-tts-word-shadow` | `--pie-text` at 35% | Shadow carrying the word over its fill |
1212
1192
 
1213
- The coordinator derives all five from the active theme's accent, text and
1214
- background — opacities clamped to a legible band, and the underline colour picked
1215
- by background luminance so the cue survives a dark scheme — then writes them
1216
- inline on the document element. Inline styles outrank any author selector, so a
1217
- host override takes `!important` and owns the contrast the derivation was
1218
- maintaining, across every scheme it ships.
1193
+ The coordinator derives all five from the active theme's `--pie-missing`,
1194
+ `--pie-text` and `--pie-background`. `--pie-missing` is the theme's
1195
+ missing-response feedback color, which reading highlights reuse as their accent.
1196
+ Opacities are clamped to a legible band, and the underline color is picked by
1197
+ background luminance so the cue survives a dark scheme. The coordinator writes
1198
+ the five inline on the document element. Inline styles outrank any author
1199
+ selector, so a host override takes `!important` and owns the contrast the
1200
+ derivation was maintaining, across every scheme it ships.
1219
1201
 
1220
1202
  ### AccessibilityCatalogResolver
1221
1203
 
1222
1204
  ```typescript
1205
+ import { AccessibilityCatalogResolver } from '@pie-players/pie-assessment-toolkit';
1206
+
1223
1207
  const resolver = new AccessibilityCatalogResolver(
1224
1208
  assessment.accessibilityCatalogs,
1225
1209
  'en-US'
@@ -1283,6 +1267,8 @@ such as TTS may still call `getAlternative(...)` with a context built by
1283
1267
  ### SSMLExtractor
1284
1268
 
1285
1269
  ```typescript
1270
+ import { SSMLExtractor } from '@pie-players/pie-assessment-toolkit';
1271
+
1286
1272
  const extractor = new SSMLExtractor();
1287
1273
 
1288
1274
  // Extract from item config
@@ -1298,11 +1284,8 @@ catalogResolver.addItemCatalogs(result.catalogs);
1298
1284
 
1299
1285
  ### Cards without an extractor
1300
1286
 
1301
- Not every catalog card is lifted out of item markup. A signed alternate is
1302
- authored or written by an importer and has no extractor at all; one such lift
1303
- existed and was removed, because nothing produced the inline form and a runtime
1304
- that could not parse the markup left the video in the visible content, showing the
1305
- accommodation to every learner regardless of eligibility.
1287
+ A signed alternate is authored or written by an importer; no extractor lifts it
1288
+ from markup.
1306
1289
 
1307
1290
  This package resolves and registers those cards through
1308
1291
  `AccessibilityCatalogResolver` and the generic media helpers it re-exports
@@ -1313,25 +1296,20 @@ needs them — signing's card validators and its resolution rules live in
1313
1296
 
1314
1297
  ### Feature policy without a placement
1315
1298
 
1316
- `ToolPolicyEngine.decideFeature(featureId)` (and
1317
- `ToolkitCoordinator.decideFeaturePolicy(featureId)`) resolve one feature id
1318
- through `PnpPolicySource`'s six-level precedence, independent of any toolbar
1319
- placement. Use it for capabilities that are not toolbar surfaces — signing is the
1320
- first — where a placement-scoped `decide(...)` would answer the wrong question:
1321
- absent because it was never placed, rather than absent because policy said no.
1322
-
1323
- `createEmptyPersonalNeedsProfile()` is the only profile this package ships, and it
1324
- grants nothing. Which capabilities a deployment grants by default is a property
1325
- of the program rather than of a capability — TTS is a universal feature in one
1326
- program and a documented accommodation in another — so it belongs in policy
1327
- configuration alongside the district and test-administration levels. Hosts that
1328
- want today's universal set take `createUniversalPersonalNeedsProfile()` from
1329
- `@pie-players/pie-default-tool-loaders`, which ships it as data.
1330
-
1331
- Nothing derives a profile from the registry any more. Doing so read registry
1332
- membership as eligibility tier — registration means "policy-addressable", not
1333
- "universal, on by default" — and had to be corrected with a compile-time list of
1334
- ids to exclude that a host could not extend for its own accommodation.
1299
+ `ToolPolicyEngine.decideFeature(featureId, scope?)` (and
1300
+ `ToolkitCoordinator.decideFeaturePolicy(featureId, scope?)`) resolve one feature id
1301
+ through `PnpPolicySource`'s PNP precedence, independent of any toolbar
1302
+ placement. Use it for capabilities that are not toolbar surfaces, such as
1303
+ signing: an item-level `decideToolPolicy(...)` also reports a tool absent when it
1304
+ was never placed, so it cannot tell a denial from a missing placement. An item
1305
+ scope applies that item's registered settings, the item restriction and
1306
+ requirement rungs; a decision without one applies no item's.
1307
+
1308
+ This package ships no profile.
1309
+ [Universal and empty profiles](../default-tool-loaders/README.md#universal-and-empty-profiles)
1310
+ covers the profiles `@pie-players/pie-default-tool-loaders` builds.
1311
+
1312
+ Registry membership makes a tool policy-addressable; it grants nothing.
1335
1313
 
1336
1314
  ### Live registry changes
1337
1315
 
@@ -1378,21 +1356,27 @@ The section player provides automatic ToolkitCoordinator integration:
1378
1356
  // - Extracts services from coordinator
1379
1357
  // - Scopes its runtime engine to `section-id`
1380
1358
  // - Provides runtime context to child components
1381
- // - Manages SSML extraction
1359
+ // - Registers item catalogs, including SSML catalogs a host extracted
1360
+ // into `config.extractedCatalogs` with `SSMLExtractor`
1382
1361
  // - Handles catalog lifecycle
1383
1362
  </script>
1384
1363
  ```
1385
1364
 
1386
1365
  ### Runtime Context Contract
1387
1366
 
1388
- The toolkit now exports a shared context key used by section-player and toolkit
1389
- components:
1367
+ Section-player and toolkit components share one runtime context. An element
1368
+ inside the toolkit's tree connects to it:
1390
1369
 
1391
1370
  ```typescript
1392
1371
  import {
1393
- assessmentToolkitRuntimeContext,
1372
+ connectToolRuntimeContext,
1394
1373
  type AssessmentToolkitRuntimeContext
1395
1374
  } from "@pie-players/pie-assessment-toolkit";
1375
+
1376
+ let runtime: AssessmentToolkitRuntimeContext | undefined;
1377
+ const disconnect = connectToolRuntimeContext(hostElement, (value) => {
1378
+ runtime = value;
1379
+ });
1396
1380
  ```
1397
1381
 
1398
1382
  `AssessmentToolkitRuntimeContext` carries ambient orchestration dependencies
@@ -1422,7 +1406,7 @@ player.section = mySection;
1422
1406
 
1423
1407
  // Internally creates:
1424
1408
  // new ToolkitCoordinator({
1425
- // assessmentId: 'section-demo-direct', // or the assessment-id attribute
1409
+ // assessmentId: runtime.assessmentId ?? section.identifier ?? 'assessment-<random>',
1426
1410
  // toolRegistry, // the player's toolRegistry, else the packaged registry
1427
1411
  // tools: player.runtime?.tools // no tools are placed when this is unset
1428
1412
  // })
@@ -1430,12 +1414,7 @@ player.section = mySection;
1430
1414
 
1431
1415
  ### Safe Custom Tool Configuration
1432
1416
 
1433
- Default behavior is now framework-owned: invalid tools/runtime initialization is handled in `pie-assessment-toolkit` without host try/catch.
1434
-
1435
- - Framework logs a deterministic console error prefix: `[pie-framework:<kind>:<source>]`
1436
- - Framework emits a canonical `framework-error` event
1437
- - Framework renders a fallback error panel instead of a blank player
1438
- - Startup tool-config validation can surface as `kind: "coordinator-init"` when the owned coordinator construction path throws.
1417
+ Invalid tool or runtime initialization is handled inside `pie-assessment-toolkit`; the host needs no try/catch. The error model, its events and hooks, and the fallback panel are in [Framework-owned error handling](../../docs/tools-and-accomodations/framework-owned-error-handling.md); host-side config patterns are in [Safe custom tool configuration](../../docs/tools-and-accomodations/safe-custom-tool-config.md).
1439
1418
 
1440
1419
  Use `createToolsConfig()` when you want to pre-validate and inspect diagnostics before mounting:
1441
1420
 
@@ -1474,147 +1453,106 @@ const coordinator = new ToolkitCoordinator({
1474
1453
  Notes:
1475
1454
  - `providers.textToSpeech` is the canonical TTS provider key.
1476
1455
  - `providers.tts` is rejected by the validation contract.
1477
- - Custom tools can provide provider-level `sanitizeConfig` and `validateConfig` hooks.
1478
- - Hosts can react to framework errors via the `framework-error` DOM event,
1479
- the `onFrameworkError(model)` callback prop, or by subscribing directly
1480
- to the package-internal bus via
1481
- `ToolkitCoordinator.subscribeFrameworkErrors(listener)`. The callback
1482
- prop fires exactly once per error, regardless of wrapper depth. Filter
1483
- by `model.kind` (e.g. `"tts-init"`, `"provider-init"`,
1484
- `"provider-register"`, `"tool-surface"`) for tool- or provider-specific
1485
- handling. Recoverable warnings remain observable but do not move section
1486
- readiness to `error`.
1487
- - See `docs/tools-and-accomodations/framework-owned-error-handling.md` for event payload and error-kind mapping details.
1456
+ - A tool registration can declare `sanitizeConfig` and `validateConfig` hooks for its `providers.<toolId>` entry.
1488
1457
 
1489
- ## Section Runtime Engine (advanced)
1458
+ ## Instrumentation and Observability
1490
1459
 
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.
1460
+ Toolkit instrumentation is provider-agnostic. It uses the shared
1461
+ `InstrumentationProvider` contract from `@pie-players/pie-players-shared`.
1521
1462
 
1522
- ## Writing a capability package
1463
+ ### Injection Path
1523
1464
 
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.
1465
+ When the toolkit is hosted by a section or assessment player, the provider
1466
+ comes from the item-player loader config:
1530
1467
 
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.
1468
+ - `runtime.player.loaderConfig.instrumentationProvider`
1536
1469
 
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.
1470
+ ### Semantics
1471
+
1472
+ - How an unset, `null` or invalid provider resolves is set out in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#provider-resolution).
1473
+ - Debug overlays can consume the same stream by composing providers with
1474
+ `CompositeInstrumentationProvider` (for example New Relic + debug panel).
1475
+ - Toolkit telemetry forwarding uses the same provider path, so tool/backend
1476
+ instrumentation is sent to production providers and is visible in debug panel
1477
+ overlays.
1542
1478
 
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.
1479
+ ### Toolkit-Owned Canonical Event Stream
1480
+
1481
+ - `pie-toolkit-runtime-owned`
1482
+ - `pie-toolkit-runtime-inherited`
1483
+ - `pie-toolkit-runtime-ready`
1484
+ - `pie-toolkit-ready`
1485
+ - `pie-toolkit-section-ready`
1486
+ - `pie-toolkit-framework-error`
1487
+
1488
+ Toolkit operational events are listed in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#operational-events).
1489
+
1490
+ The toolkit emits toolkit lifecycle events only; section and assessment events
1491
+ stay in their own layers, so no event has two emitters. The event bridge's
1492
+ optional `dedupeWindowMs` drops repeats within a window and is off by default.
1493
+
1494
+ ## Section Runtime Engine (advanced)
1495
+
1496
+ The section runtime engine is the section player's stage chain: a pure FSM that
1497
+ turns the cohort a layout shows (its `(sectionId, attemptId)` pair), the
1498
+ controller resolving and the readiness signals the layout derives into
1499
+ `pie-stage-change` and `pie-loading-complete`.
1500
+ The section player's layout kernel owns one per layout element, and it is the
1501
+ only stage emitter. `<pie-assessment-toolkit>` emits no stages: its
1502
+ `SectionControllerBinding` resolves the section's controller, which the toolkit
1503
+ announces with `toolkit-ready`, and the toolkit publishes framework errors.
1504
+
1505
+ ### Entry point
1506
+
1507
+ `@pie-players/pie-assessment-toolkit/runtime/engine` is the stable entry for a
1508
+ host that drives a section's stage chain. It carries `SectionRuntimeEngine` and
1509
+ the vocabulary of its surface: the `SectionEngineInput`s `dispatchInput` takes,
1510
+ the `SectionEngineOutput`s `subscribe` delivers, the `SectionEngineState` and
1511
+ `SectionEnginePhase` `getState` returns, the cohort helpers (`makeCohort`,
1512
+ `cohortsEqual`), the runtime config types with
1513
+ `resolveSectionEngineRuntimeState`, and the readiness signals. The engine core, its adapter and its DOM bridge have no
1514
+ entry: a host reaches them through the facade.
1515
+
1516
+ ### Stage chain
1517
+
1518
+ A cohort moves through `composed`, `engine-ready` and `interactive`, and ends at
1519
+ `disposed` on a cohort change or unmount. `engine-ready` follows the
1520
+ `section-controller-resolved` input, `interactive` the readiness signals that
1521
+ satisfy the readiness mode. A readiness update that reports `runtimeError`
1522
+ before `interactive` ends the chain: the first stage the cohort did not reach is
1523
+ `failed` and the rest up to `interactive` are `skipped`. The section player sets
1524
+ `runtimeError` from a non-recoverable `framework-error`, so a host waiting on
1525
+ `engine-ready` or `interactive` always hears an answer. `pie-loading-complete`
1526
+ fires once per cohort, on the condition that opens `interactive`: the section
1527
+ controller is ready and the element pre-warm for the current items has
1528
+ resolved. The items load after it; the controller's `section-loading-complete`
1529
+ marks them loaded.
1565
1530
 
1566
1531
  ### Common-host wiring example
1567
1532
 
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:
1533
+ Most hosts never construct the engine: the section-player layout elements do,
1534
+ and a host-built section layout takes its engine from
1535
+ `pie-section-player-kernel-host` ([custom section layouts](../../docs/section-player/custom-layouts.md)).
1536
+ Use the facade for a section renderer outside the section player. The shape
1537
+ mirrors the section-player kernel:
1572
1538
 
1573
1539
  ```ts
1574
- import { ContextProvider } from "@pie-players/pie-context";
1575
1540
  import {
1576
1541
  SectionRuntimeEngine,
1577
- sectionRuntimeEngineHostContext,
1578
- } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1579
- import {
1580
- FrameworkErrorBus,
1581
1542
  makeCohort,
1582
- } from "@pie-players/pie-assessment-toolkit/runtime/internal";
1543
+ } from "@pie-players/pie-assessment-toolkit/runtime/engine";
1583
1544
 
1584
- const bus = new FrameworkErrorBus();
1585
1545
  const engine = new SectionRuntimeEngine();
1586
1546
 
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
- });
1595
-
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
- },
1607
- });
1608
- engineProvider.connect();
1547
+ // 1. Attach to the layout host. `sourceCe` is stamped onto every event.
1548
+ engine.attachHost({ host: layoutHostElement, sourceCe: "my-custom-layout" });
1609
1549
 
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
1550
+ // 2. (Optional) Subscribe to the output batches the DOM bridge dispatches.
1551
+ engine.subscribe((outputs) => {
1552
+ // `stage-change` and `loading-complete` outputs
1615
1553
  });
1616
1554
 
1617
- // 4. Drive the engine. Use real `SectionEngineInput` shapes:
1555
+ // 3. Drive the engine with `SectionEngineInput`s.
1618
1556
  const cohort = makeCohort({ sectionId, attemptId });
1619
1557
  engine.dispatchInput({
1620
1558
  kind: "initialize",
@@ -1624,7 +1562,10 @@ engine.dispatchInput({
1624
1562
  itemCount,
1625
1563
  });
1626
1564
 
1627
- // On loading-progress / readiness signal updates:
1565
+ // When the toolkit's `section-ready` delivers the cohort's section controller:
1566
+ engine.dispatchInput({ kind: "section-controller-resolved" });
1567
+
1568
+ // On readiness signal updates:
1628
1569
  engine.dispatchInput({
1629
1570
  kind: "update-readiness-signals",
1630
1571
  signals: {
@@ -1633,46 +1574,58 @@ engine.dispatchInput({
1633
1574
  allLoadingComplete,
1634
1575
  runtimeError,
1635
1576
  },
1636
- loadedCount,
1637
1577
  itemCount,
1638
1578
  mode: "progressive",
1639
1579
  });
1640
1580
 
1641
- // On unmount:
1642
- engineProvider.disconnect();
1581
+ // On unmount; emits `disposed` for the active cohort.
1643
1582
  engine.dispose();
1644
1583
  ```
1645
1584
 
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.
1585
+ The adapter dispatches `pie-stage-change` and `pie-loading-complete` on `host`,
1586
+ bubbling and composed, as the toolkit dispatches its own events. Framework
1587
+ errors are not an engine output. The toolkit that owns the coordinator publishes
1588
+ each one once: one bubbling, composed `framework-error` event, which reaches the
1589
+ layout host and `document`, and one `onFrameworkError` call. A coordinator the
1590
+ host passes in reports through the same surfaces.
1591
+ `packages/section-player/tests/section-player-event-delivery.spec.ts` pins these
1592
+ counts.
1661
1593
 
1662
- Hosts should listen to `pie-stage-change` (with the readiness detail also
1663
- available via the kernel's `selectReadiness()`) and `pie-loading-complete`.
1594
+ ## Writing a capability package
1664
1595
 
1665
- ## State Separation: Tool State vs Session Data
1596
+ `@pie-players/pie-assessment-toolkit/tools/registration` is the stable entry a
1597
+ capability package imports: the `ToolRegistration` contract, the
1598
+ `ToolProviderApi` its provider descriptor creates, the surface and content
1599
+ dependency types, the context predicates, `createToolElement`, `resolveToolTag`
1600
+ and the toolbar registration helpers. A renderer that puts registrations on
1601
+ screen uses the same entry for `createToolSurfaceHost` and
1602
+ `resolveContentCapabilities`.
1666
1603
 
1667
- The toolkit enforces a clear separation between ephemeral tool state and persistent session data:
1604
+ Import it rather than the package root: the root pulls in `ToolkitCoordinator`,
1605
+ `TTSService` and the components, none of which a registration needs, and a
1606
+ capability bundle that inlines them ends up with a second `ToolRegistry` class
1607
+ that fails every `instanceof` across the host boundary. Mark the toolkit external
1608
+ in the package's build with a pattern that covers subpaths, not a bare specifier.
1668
1609
 
1669
- ### Tool State (Ephemeral - ElementToolStateStore)
1610
+ `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1611
+ registration, a content resolver, its own custom element, and no edit to any
1612
+ generic package. The [default-tool-loaders README](../default-tool-loaders/README.md)
1613
+ covers how a deployment then composes it in, and the
1614
+ [Tool Registry Reference](docs/TOOL_REGISTRY.md) the registration and
1615
+ host-surface contracts.
1616
+
1617
+ ## State separation
1618
+
1619
+ The toolkit keeps tool state and PIE session data in separate stores.
1670
1620
 
1671
- **Client-only**, never sent to server for scoring:
1621
+ ### Tool state (ElementToolStateStore)
1622
+
1623
+ Never scored; it leaves the browser only through the coordinator's
1624
+ `saveToolState` hook ([Persistence Integration](#persistence-integration)):
1672
1625
 
1673
1626
  ```typescript
1674
1627
  {
1675
- "demo-assessment:section-1:question-1:mc1": {
1628
+ "demo-assessment:section-1:attempt-1:question-1:mc1": {
1676
1629
  "answerEliminator": {
1677
1630
  "eliminatedChoices": ["choice-b", "choice-d"]
1678
1631
  },
@@ -1689,9 +1642,9 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1689
1642
  - Tool preferences
1690
1643
  - UI state
1691
1644
 
1692
- ### PIE Session Data (Persistent)
1645
+ ### PIE session data
1693
1646
 
1694
- **Sent to server** for scoring:
1647
+ The host persists it, and its backend scores it:
1695
1648
 
1696
1649
  ```typescript
1697
1650
  {
@@ -1711,11 +1664,7 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1711
1664
 
1712
1665
  ## Examples
1713
1666
 
1714
- See the [section-demos](../../apps/section-demos/) for complete examples:
1715
-
1716
- - **Three Questions Demo**: Element-level answer eliminator with state persistence
1717
- - **TTS Integration Demo**: Toolkit coordinator with TTS service
1718
- - **Paired Passages Demo**: Multi-section assessment with cross-section state
1667
+ See [section-demos](../../apps/section-demos/) for complete examples, among them "Demo 3: Three Questions, One Passage", "Demo 4: TTS with SSML Extraction" and "Two Passages with One Question".
1719
1668
 
1720
1669
  ## TypeScript Support
1721
1670
 
@@ -1725,8 +1674,7 @@ Full TypeScript definitions included:
1725
1674
  import type {
1726
1675
  ToolkitCoordinatorApi,
1727
1676
  ElementToolStateStoreApi,
1728
- ToolkitCoordinatorConfig,
1729
- ToolkitServiceBundle
1677
+ ToolkitCoordinatorConfig
1730
1678
  } from '@pie-players/pie-assessment-toolkit';
1731
1679
  ```
1732
1680
 
@@ -1740,31 +1688,27 @@ from tool configuration. Two sanitization layers apply:
1740
1688
  `pie-item-player`. See
1741
1689
  [pie-item-player README](../item-player/README.md#content-trust-boundary)
1742
1690
  for the `trust-markup` opt-out and the `sanitizeMarkup` override.
1743
- As a post-sanitization step, every authored `<img>` outside a `pie-*`
1744
- custom element is wrapped in `<span class="pie-image-scroll">` so
1745
- overwide images surface a horizontal scrollbar instead of being
1746
- clipped by the section layout's `overflow-x: hidden` ancestors
1747
- (PIE-94 / WCAG 1.4.10 Reflow at 400% zoom). The wrapper is
1748
- keyboard-scrollable (`tabindex="0"`, `role="region"`) and carries
1749
- the image's `alt` text in its `aria-label`; matching CSS lives in
1750
- `@pie-players/pie-theme` (`components.css`). Authored `<table>`
1751
- elements outside a `pie-*` custom element get the same treatment via
1752
- `<div class="pie-table-scroll">` so wide data grids reflow into a
1753
- horizontally scrollable region; the wrapper's `aria-label` is derived
1754
- from the table's `aria-label` / `aria-labelledby` / `<caption>`.
1691
+ The player also wraps overwide images and tables in scrollable regions
1692
+ ([reflow wrappers](../item-player/README.md#reflow-wrappers)).
1755
1693
  - **Tool icons and SSML** - tool-registered icon markup is parsed and
1756
1694
  DOMPurified inside the toolbar at render time; SSML payloads are
1757
1695
  restricted to an allow-listed subset of SSML tags/attributes before
1758
1696
  being forwarded to TTS providers. Do not ship tools that rely on raw
1759
1697
  `<script>` or event-handler attributes in their icon strings.
1760
1698
 
1699
+ [Security](../../docs/security/readme.md) sets out the full trust boundary and
1700
+ the host's obligations.
1701
+
1761
1702
  ## Related Documentation
1762
1703
 
1763
- - **[Tool Registry Architecture](docs/TOOL_REGISTRY.md)** - ⭐ NEW - Registry-based tool management and QTI 3.0 PNP support
1764
- - **[PNP Configuration Guide](docs/PNP_CONFIGURATION.md)** - ⭐ NEW - How to configure student profiles, district policies, and governance rules
1765
- - [ToolkitCoordinator Architecture](../../docs/architecture/architecture.md#toolkitcoordinator-centralized-service-management) - Design decisions and patterns
1704
+ - [Tool Registry Reference](docs/TOOL_REGISTRY.md) - Registry-based tool management and AfA PNP 3.0 profile support
1705
+ - [PNP Configuration Guide](docs/PNP_CONFIGURATION.md) - How to configure student profiles, district policies, and governance rules
1706
+ - [Configuring Tools](../../docs/tools-and-accomodations/tool_provider_system.md) - Placement, policy and provider configuration
1707
+ - [Toolkit Tool Host Contract](../../docs/tools-and-accomodations/tool_host_contract.md) - Runtime guarantees between hosts and toolkit-managed tools
1708
+ - [Tools and Accommodations Architecture](../../docs/tools-and-accomodations/architecture.md) - Tool design, scopes and activation
1709
+ - [Assessment toolkit in the architecture overview](../../docs/architecture/architecture.md#assessment-toolkit) - Where the toolkit sits among the players
1766
1710
  - [Section Player README](../section-player/README.md) - Section player integration
1767
- - [Section Player Architecture](../section-player/ARCHITECTURE.md#layered-runtime-engine-post-m7) - Layered runtime engine, kernel/toolkit wiring, lifecycle emit invariant
1711
+ - [Section Player Architecture](../section-player/ARCHITECTURE.md#layered-runtime-engine) - Layered runtime engine, kernel/toolkit wiring, lifecycle emit invariant
1768
1712
  - [Framework-Owned Error Handling](../../docs/tools-and-accomodations/framework-owned-error-handling.md) - Canonical framework error model/events and fallback behavior
1769
1713
  - [Safe Custom Tool Configuration](../../docs/tools-and-accomodations/safe-custom-tool-config.md) - Host-side config patterns and validation guidance
1770
1714
  - [Architecture Overview](../../docs/architecture/architecture.md) - Complete system architecture