@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +451 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-asenkvx6.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +32 -30
  23. package/dist/policy/core/decision-types.d.ts +51 -34
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +7 -3
  27. package/dist/policy/core/pnp-policy-inputs.js +17 -33
  28. package/dist/policy/core/provenance.d.ts +2 -2
  29. package/dist/policy/core/tool-parameters.d.ts +12 -0
  30. package/dist/policy/core/tool-parameters.js +27 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
  33. package/dist/policy/sources/PnpPolicySource.js +57 -29
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +51 -57
  66. package/dist/services/TTSService.js +323 -284
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +83 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +51 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +39 -24
  83. package/dist/services/tool-context.js +158 -92
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +2 -0
  98. package/dist/services/toolbar-decided-tools.js +2 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +13 -22
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +7 -6
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. 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
 
@@ -21,133 +49,36 @@ Server-backed TTS (`backend: "server"`) loads
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
52
+ ## Capabilities
43
53
 
44
- await ttsService.initialize(new BrowserTTSProvider());
45
- ttsService.setCatalogResolver(catalogResolver);
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
46
67
 
47
- // Pass all services separately
48
- player.ttsService = ttsService;
49
- player.toolCoordinator = toolCoordinator;
50
- // ...
51
- ```
68
+ ## Architecture overview
52
69
 
53
- **After** (coordinator orchestrates):
54
- ```typescript
55
- import {
56
- createPackagedToolRegistry,
57
- DEFAULT_TOOL_MODULE_LOADERS,
58
- } from '@pie-players/pie-default-tool-loaders';
59
-
60
- // Create one coordinator with configuration
61
- const toolRegistry = createPackagedToolRegistry({
62
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
63
- });
64
- const toolkitCoordinator = new ToolkitCoordinator({
65
- assessmentId: 'my-assessment',
66
- toolRegistry,
67
- tools: {
68
- providers: {
69
- textToSpeech: { enabled: true, backend: 'browser' },
70
- // Desmos needs an application key; see "Simple Default" below.
71
- calculator: { enabled: true }
72
- },
73
- placement: {
74
- section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
75
- item: ['calculator', 'textToSpeech', 'answerEliminator'],
76
- passage: ['textToSpeech']
77
- }
78
- }
79
- });
80
-
81
- // Pass single coordinator to section-player through runtime
82
- player.runtime = { ...(player.runtime ?? {}), coordinator: toolkitCoordinator };
83
- ```
84
-
85
- ## What Does It Solve?
86
-
87
- - **Centralized service management**: One coordinator owns all toolkit services
88
- - **Tool coordination**: z-index management, visibility state, element-level state
89
- - **Accommodation support**: IEP/504 tool configuration logic
90
- - **TTS + annotation coordination**: Prevent conflicts between highlights
91
- - **Event communication**: Standard contracts between components
92
- - **Accessibility theming**: Consistent high-contrast, font sizing
93
- - **State separation**: Ephemeral tool state separate from persistent session data
94
-
95
- ## Instrumentation and Observability
96
-
97
- Toolkit instrumentation is provider-agnostic and additive. It uses the shared
98
- `InstrumentationProvider` contract from `@pie-players/pie-players-shared`.
99
-
100
- ### Injection Path
101
-
102
- When toolkit is hosted by section/assessment player flows, the canonical
103
- provider path is the item-player loader config:
104
-
105
- - `runtime.player.loaderConfig.instrumentationProvider`
106
-
107
- ### Semantics
108
-
109
- - With `trackPageActions: true`, missing/`undefined` providers use the default New Relic provider path.
110
- - `instrumentationProvider: null` explicitly disables instrumentation.
111
- - Invalid provider objects are ignored (optional debug warning), also no-op.
112
- - Existing `item-player` behavior remains the compatibility anchor.
113
- - Debug overlays can consume the same stream by composing providers with
114
- `CompositeInstrumentationProvider` (for example New Relic + debug panel).
115
- - Toolkit telemetry forwarding uses the same provider path, so tool/backend
116
- instrumentation is sent to production providers and is visible in debug panel
117
- overlays.
118
-
119
- ### Toolkit-Owned Canonical Event Stream
120
-
121
- - `pie-toolkit-runtime-owned`
122
- - `pie-toolkit-runtime-inherited`
123
- - `pie-toolkit-runtime-ready`
124
- - `pie-toolkit-ready`
125
- - `pie-toolkit-section-ready`
126
- - `pie-toolkit-framework-error`
127
-
128
- Toolkit tool/backend operational stream:
129
-
130
- - `pie-tool-init-start|success|error`
131
- - `pie-tool-backend-call-start|success|error`
132
- - `pie-tool-library-load-start|success|error`
133
-
134
- Ownership boundary: toolkit emits toolkit lifecycle semantics only. Section and
135
- assessment semantic streams stay in their own layers to avoid overlap. Bridge
136
- dedupe is a safety net, not a substitute for clear ownership.
137
-
138
- ## Architecture Overview
139
-
140
- 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.
141
72
 
142
73
  ### Core Principles
143
74
 
144
- 1. **Centralized Coordination**: ToolkitCoordinator orchestrates all services
75
+ 1. **Centralized Coordination**: ToolkitCoordinator owns the services, and players reach them through it
145
76
  2. **Composable Services**: Import only what you need (or use coordinator for convenience)
146
77
  3. **No Framework Lock-in**: Works with any JavaScript framework
147
- 4. **Product Control**: Products control navigation, layout and backend. Session persistence defaults to `localStorage` at the section and assessment layers, stored per attempt id and inactive without one, and a product replaces either through its persistence hook (`createSectionSessionPersistence` on the coordinator, `createAssessmentSessionPersistence` on the assessment player)
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)
148
79
  5. **Standard Contracts**: Well-defined event types for component communication
149
- 6. **Element-Level Granularity**: Tool state tracked per PIE element, not per item
150
- 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
151
82
 
152
83
  ## Configuration: attributes and a constructed coordinator
153
84
 
@@ -170,20 +101,16 @@ than the element inputs builds the coordinator itself.
170
101
  ```
171
102
 
172
103
  - **A constructed `ToolkitCoordinator` (or a
173
- `runtime` object on consumer CEs).** Use this for advanced cases: composed
104
+ `runtime` object on the player custom elements).** Use this for advanced cases: composed
174
105
  configuration, dynamic overrides, runtime mutation, fields without an
175
106
  attribute, or anything that benefits from being a single typed
176
107
  object passed by reference. Example:
177
108
 
178
109
  ```ts
179
- import {
180
- createPackagedToolRegistry,
181
- DEFAULT_TOOL_MODULE_LOADERS,
182
- } from "@pie-players/pie-default-tool-loaders";
110
+ import { ToolkitCoordinator } from "@pie-players/pie-assessment-toolkit";
111
+ import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
183
112
 
184
- const toolRegistry = createPackagedToolRegistry({
185
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
186
- });
113
+ const toolRegistry = createPackagedToolRegistry();
187
114
  const coordinator = new ToolkitCoordinator({
188
115
  assessmentId: "my-assessment",
189
116
  toolRegistry,
@@ -211,33 +138,36 @@ callbacks; attributes carry section identity, layout controls, `nds-icons`,
211
138
  `locale` and `tool-config-strictness`. Unset inputs take their documented
212
139
  defaults.
213
140
 
214
- `<pie-assessment-toolkit>` takes the same fields as its own properties,
215
- with `assessment-id`, `nds-icons`, `locale` and `tool-config-strictness` as
216
- attributes, and reads `pnpEnforcement` from `tools.pnpEnforcement`.
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`.
217
147
 
218
148
  ### Canonical attribute set
219
149
 
220
150
  - Identity: `assessment-id` on the toolkit, `section-id`, `attempt-id`
221
- - Runtime config on section-player CEs: `runtime`
222
- - Toolkit-only object properties: `tools`, `tool-registry`, `coordinator`,
151
+ - Runtime config on section-player custom elements: `runtime`
152
+ - Toolkit-only object properties: `tools`, `toolRegistry`, `coordinator`,
223
153
  `accessibility`
224
154
  - Interface: `nds-icons`, `locale`
225
- - Diagnostics: `tool-config-strictness`, `debug`. Framework-error
155
+ - Diagnostics: `tool-config-strictness`, and `debug` on the section-player layouts. Framework-error
226
156
  delivery is via the `onFrameworkError` callback (a toolkit property,
227
157
  `runtime.onFrameworkError` on a section player) and the
228
- `framework-error` DOM event dispatched on the layout CE host.
158
+ `framework-error` DOM event dispatched on the layout element.
229
159
 
230
160
  Inputs outside the runtime config:
231
161
 
232
162
  - Identity (`section-id`, `attempt-id`, `section`): per-attempt host
233
163
  state, not configuration.
234
- - Layout-only shell knobs on the section-player layout CEs
164
+ - Layout-only shell knobs on the section-player layout elements
235
165
  (`show-toolbar`, `toolbar-position`, `narrow-layout-breakpoint`,
236
- `split-pane-collapse-strategy`): layout-CE rendering concerns.
166
+ `split-pane-collapse-strategy`): rendering concerns of the layout element.
237
167
  - Per-region toolbar tool placement: hosts populate
238
168
  `tools.placement.item` / `tools.placement.passage` (object form) or
239
169
  `runtime.tools.placement.{item,passage}` directly.
240
- - Runtime-only keys on the section-player layout CEs
170
+ - Runtime-only keys on the section-player layout elements
241
171
  (`createSectionController`, `isolation`): accepted only via
242
172
  `runtime.<key>`. Section-player layouts forward both to the wrapped
243
173
  toolkit as properties. `<pie-assessment-toolkit>` keeps
@@ -245,42 +175,46 @@ Inputs outside the runtime config:
245
175
  attribute or a property. A `coordinator` passed to it wins over an
246
176
  outer toolkit's.
247
177
 
248
- ### When to add an attribute
178
+ ### Adding an attribute (contributors)
249
179
 
250
- Add an attribute only if all of the following hold:
180
+ A new input becomes an attribute only if all of the following hold:
251
181
 
252
182
  - It is a common case that hosts set without composing a `ToolkitCoordinator`
253
183
  / `runtime` object.
254
184
  - Its value is a primitive or small typed object that round-trips through
255
185
  HTML attributes (string, boolean-like, number; structured data passes via
256
186
  property assignment).
257
- - 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
258
188
  deliberate documented exclusion.
259
189
 
260
190
  Otherwise expose it through the configuration object only.
261
191
 
262
192
  ## Quick Start
263
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
+
264
203
  ### Option 1: Use ToolkitCoordinator (Recommended)
265
204
 
266
205
  ```typescript
267
206
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
268
- import {
269
- createPackagedToolRegistry,
270
- DEFAULT_TOOL_MODULE_LOADERS,
271
- } from '@pie-players/pie-default-tool-loaders';
207
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
272
208
 
273
209
  // Create coordinator with configuration
274
- const toolRegistry = createPackagedToolRegistry({
275
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
276
- });
210
+ const toolRegistry = createPackagedToolRegistry();
277
211
  const coordinator = new ToolkitCoordinator({
278
212
  assessmentId: 'demo-assessment',
279
213
  toolRegistry,
280
214
  tools: {
281
215
  providers: {
282
216
  textToSpeech: { enabled: true, backend: 'browser' },
283
- // Desmos needs an application key; see "Simple Default" below.
217
+ // Desmos needs an API key; see "Calculator providers" below.
284
218
  calculator: { enabled: true }
285
219
  },
286
220
  placement: {
@@ -306,7 +240,7 @@ const toolState = coordinator.elementToolStateStore.getAllState();
306
240
 
307
241
  ### Controller Event Subscriptions (Helper First)
308
242
 
309
- 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:
310
244
 
311
245
  ```typescript
312
246
  const unsubscribeItem = coordinator.subscribeItemEvents({
@@ -327,7 +261,7 @@ unsubscribeItem?.();
327
261
  unsubscribeSection?.();
328
262
  ```
329
263
 
330
- Behavior pins (PIE-512 Phase D):
264
+ Subscription behavior:
331
265
 
332
266
  - Subscribe **after** the first `getOrCreateSectionController(...)` resolves; calling subscribe before any cohort exists throws.
333
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.
@@ -339,85 +273,12 @@ Use `subscribeSectionEvents(...)` when you need advanced/custom filtering mixes.
339
273
 
340
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.
341
275
 
342
- #### Migrating from `<0.3.35` (BREAKING — pre-1.0)
343
-
344
- `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.
345
-
346
- If your host code looked like this:
347
-
348
- ```typescript
349
- const unsub = coordinator.subscribeItemEvents({
350
- sectionId: 'section-1',
351
- attemptId: 'attempt-1',
352
- listener: handleEvent,
353
- });
354
- ```
355
-
356
- Update it to drop `sectionId` / `attemptId`:
357
-
358
- ```typescript
359
- const unsub = coordinator.subscribeItemEvents({
360
- listener: handleEvent,
361
- });
362
- ```
363
-
364
- What this means in practice for typed integrations:
365
-
366
- - **TypeScript breaking change.** `SectionEventSubscriptionArgs`, `SectionItemEventSubscriptionArgs`, and `SectionScopedEventSubscriptionArgs` no longer declare `sectionId?` / `attemptId?` properties. Any host that imports these arg types directly and passes those keys will fail to compile after upgrade. **Action required.**
367
- - **Runtime is tolerant.** The runtime silently ignores extra unknown properties, so an untyped or lightly-typed call site that still passes `sectionId` / `attemptId` continues to work without source changes. The args have **no effect** at runtime — the subscription always follows the active cohort.
368
- - **New precondition.** `subscribe*` throws until the first `getOrCreateSectionController(...)` call; a listener added while a section is starting binds when that section becomes active. Subscribe **after** the first `getOrCreateSectionController(...)` resolves. Subscribing on `toolkit-ready` alone is no longer sufficient — though in practice the section player emits `toolkit-ready` *after* its first `getOrCreateSectionController(...)` resolves, so a `toolkit-ready` anchor is safe in section-player hosts.
369
- - **Cohort migration is automatic.** If your wrapper previously re-subscribed on every navigation to keep listeners alive across sections, that wiring is no longer needed (and should be removed). A single subscribe call after the first controller-resolve is now enough — the listener migrates automatically and is replayed the new cohort's snapshot on every transition.
370
- - **Watch for double-replay if you re-subscribe on every `toolkit-ready`.** Hosts that detached and re-subscribed on every `toolkit-ready` event (the correct pre-Phase D pattern, since each subscription was pinned to a `sectionId`) will now observe **two snapshot replays per navigation**: one delivered automatically when Phase D migrates the existing listener to the new active cohort, and a second when the manual re-subscribe attaches a fresh listener that replays again. Listener handlers that are not strictly idempotent will fire twice — analytics `pageAction`s, non-Set counters, side-effecting hydration. The fix is a one-line guard (`if (this.controllerUnsubscribe) return;`) so the subscribe runs only on the first `toolkit-ready`.
371
- - **For intentionally-pinned subscriptions to inactive sections** (e.g. a host UI that wants to keep watching section A while the user views section B), the helper API does not support that pattern by design. Use `coordinator.getSectionController({ sectionId, attemptId })` and subscribe directly on the controller handle (`controller.subscribe?.(...)`) — that binding is pinned to one controller instance and does not migrate.
372
-
373
- 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.
374
-
375
- #### Pre-Phase D vs Phase D wrapper pattern
376
-
377
- ```typescript
378
- // BEFORE (pre-Phase D): rebind for every section change because the
379
- // subscription was pinned to a sectionId.
380
- public handleToolkitReady(event: Event): void {
381
- const coordinator = (event as CustomEvent).detail?.coordinator;
382
- if (!coordinator) return;
383
- this.controllerUnsubscribe?.(); // detach prior pin
384
- const itemUnsub = coordinator.subscribeItemEvents({
385
- sectionId: this.sectionId,
386
- listener: handleItemEvent,
387
- });
388
- const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
389
- sectionId: this.sectionId,
390
- listener: handleSectionEvent,
391
- });
392
- this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
393
- }
394
- ```
395
-
396
- ```typescript
397
- // AFTER (Phase D): subscribe once; the listener follows the active
398
- // cohort across all subsequent navigation.
399
- public handleToolkitReady(event: Event): void {
400
- const coordinator = (event as CustomEvent).detail?.coordinator;
401
- if (!coordinator) return;
402
- this.toolkitCoordinator = coordinator;
403
- if (this.controllerUnsubscribe) return; // already subscribed; do nothing on re-fire
404
- const itemUnsub = coordinator.subscribeItemEvents({
405
- listener: handleItemEvent,
406
- });
407
- const sectionUnsub = coordinator.subscribeSectionLifecycleEvents({
408
- listener: handleSectionEvent,
409
- });
410
- this.controllerUnsubscribe = () => { itemUnsub?.(); sectionUnsub?.(); };
411
- }
412
- ```
413
-
414
276
  ### Option 2: Create Services Manually (Advanced)
415
277
 
416
278
  ```typescript
417
279
  import {
418
280
  TTSService,
419
281
  BrowserTTSProvider,
420
- ToolCoordinator,
421
282
  HighlightCoordinator,
422
283
  AccessibilityCatalogResolver,
423
284
  ElementToolStateStore
@@ -425,7 +286,6 @@ import {
425
286
 
426
287
  // Initialize each service independently
427
288
  const ttsService = new TTSService();
428
- const toolCoordinator = new ToolCoordinator();
429
289
  const highlightCoordinator = new HighlightCoordinator();
430
290
  const elementToolStateStore = new ElementToolStateStore();
431
291
  const catalogResolver = new AccessibilityCatalogResolver([], 'en-US');
@@ -439,15 +299,15 @@ ttsService.setCatalogResolver(catalogResolver);
439
299
  No player element takes services one by one. The section and assessment
440
300
  players reach services only through a coordinator, `runtime.coordinator` on a
441
301
  section player and `coordinator` on an assessment player; manually created
442
- services serve host code that drives them directly.
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`.
443
304
 
444
305
  ### Without a Section Player
445
306
 
446
- `<pie-assessment-toolkit>` needs no section. Bind none and it provides tools,
447
- policy and services to the item toolbars and item players inside it, which is
448
- how the toolkit accompanies a plain item player. `<pie-item-scope>` holds the
449
- item for its tools, as it does in a section player's card. The tree this
450
- builds:
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:
451
311
 
452
312
  ```html
453
313
  <pie-assessment-toolkit>
@@ -465,20 +325,17 @@ scope registers as soon as it mounts, and that first registration starts the
465
325
  coordinator from the toolkit's inputs at that moment; see the binding rules
466
326
  below.
467
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
+
468
330
  ```typescript
469
331
  import '@pie-players/pie-assessment-toolkit/components/pie-assessment-toolkit-element';
470
332
  import '@pie-players/pie-assessment-toolkit/components/item-scope-element';
471
333
  import '@pie-players/pie-assessment-toolkit/components/item-toolbar-element';
472
334
  import '@pie-players/pie-item-player';
473
- import {
474
- createPackagedToolRegistry,
475
- DEFAULT_TOOL_MODULE_LOADERS,
476
- } from '@pie-players/pie-default-tool-loaders';
335
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
477
336
 
478
337
  // With no section player, the registry's loaders are what define the tool elements.
479
- const toolRegistry = createPackagedToolRegistry({
480
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
481
- });
338
+ const toolRegistry = createPackagedToolRegistry();
482
339
 
483
340
  const toolkit = document.createElement('pie-assessment-toolkit');
484
341
  toolkit.tools = {
@@ -501,76 +358,109 @@ toolkit.append(scope);
501
358
  container.append(toolkit);
502
359
  ```
503
360
 
504
- The toolbars and tools inside the scope take the item and its id from it, and
505
- act on the scope, or on the element its `scopeElement` property names;
506
- `textToSpeech` reads the scope's `[data-region="content"]` first. A toolbar's
507
- own `item`, `item-id` and `scopeElement` still override the scope, and its
508
- `toolRegistry` the toolkit's. The scope
509
- registers the item with the toolkit once it finds one above it, so it may mount
510
- first, and the toolkit files the item's accessibility catalogs, which read-aloud
511
- speaks in place of the markup they name. Item player events pass through the
512
- scope unchanged, and the session stays the item player's. A registry built
513
- without `toolModuleLoaders` renders toolbar buttons whose tool elements never
514
- load.
515
-
516
- A profile change is a new `assessment` value; the toolbars re-derive on the
517
- policy change it emits. The toolkit announces `runtime-ready`, with
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
518
385
  `{ runtimeId, coordinator, ownership }`, once per coordinator, with or without a
519
386
  section. Without one, the coordinator starts at the first item that registers,
520
387
  and a host reads its readiness from `coordinator.waitUntilReady()` or
521
- `isReady()`; `toolkit-ready` and `section-ready` wait for a bound section. The
522
- toolkit emits no stage events: `pie-stage-change` is the section player's. A
523
- host that holds
524
- the coordinator from `runtime-ready`, or passes its own as `coordinator`,
525
- changes the profile with `coordinator.updateAssessment(...)`; the toolkit
526
- applies its `assessment` property only to a coordinator it owns. The
527
- `calculator-pnp` section demo composes the owned-coordinator form.
528
-
529
- The toolkit builds its own coordinator at mount from `tools`, `enabled-tools`,
530
- `assessment-id`, `accessibility`, `lazy-init`, `tool-config-strictness` and
531
- `toolRegistry`. Its first content binds it: the section, or without one the
532
- first item scope that registers. Content that arrives after one of those inputs
533
- changed binds a coordinator rebuilt from the current values, and the toolbars
534
- move to it. After that, a change to them is reported once in the console and
535
- does not reach the coordinator, except a `toolRegistry` given to a toolkit that
536
- had none, which the coordinator adopts in place; `tools.pnpEnforcement`,
537
- `assessment` and `toolContextResolvers` apply to it at any
538
- time. That
539
- coordinator reports feature policy asked with no assessment bound only while
540
- `tools.pnpEnforcement` is `on`: a toolkit given no `assessment` and no enforcement
541
- has asked for no accommodation.
542
-
543
- A toolkit nested in another inherits the outer one's coordinator when the outer
544
- one has a coordinator by the time the inner one connects, which holds for both
545
- mounted together. Otherwise the inner one builds its own and keeps it, and
546
- reports an outer coordinator arriving later once in the console. `isolation`
547
- `"force"` keeps a nested toolkit on its own coordinator by design.
548
-
549
- Text-to-speech starts at the toolkit's first content, once the section composes
550
- or the first item scope registers, and `coordinator.waitUntilReady()` waits for it. With `lazy-init` it starts at the first read-aloud instead, unless policy
551
- grants it. A tool provider or text-to-speech that fails to start is a
552
- recoverable framework error: the tool reports itself unavailable and the
553
- assessment goes on. When policy grants the tool, through an item or district
554
- requirement or a profile support, the failure is fatal, including one that a
555
- later policy change grants.
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.
556
432
 
557
433
  ## Tool Configuration Model
558
434
 
559
- The toolkit uses one canonical `tools` model with three concerns:
435
+ The toolkit uses one canonical `tools` model with four keys:
560
436
 
561
- - `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.
562
439
  - `placement`: where tools appear (`section`, `item`, `passage`). Each tool
563
440
  declares the levels it supports; `calculator` is item-only, and a tool placed
564
441
  at a level it does not support fails validation, which throws under the
565
442
  default `toolConfigStrictness: "error"`.
566
- - `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, and `createPersonalNeedsProfileFromAfa` in
453
+ `@pie-players/pie-default-tool-loaders` converts an AfA record
454
+ ([AfA PNP 3.0 records](docs/PNP_CONFIGURATION.md#afa-pnp-30-records)).
567
455
 
568
456
  Example:
569
457
 
570
458
  ```typescript
571
459
  tools: {
572
460
  policy: {
461
+ // Only these tools may appear...
573
462
  allowed: ['calculator', 'textToSpeech', 'answerEliminator', 'graph', 'periodicTable'],
463
+ // ...and graph never does.
574
464
  blocked: ['graph']
575
465
  },
576
466
  placement: {
@@ -589,25 +479,38 @@ tools: {
589
479
  }
590
480
  ```
591
481
 
592
- ### Scope and Lifecycle
482
+ With this configuration the section toolbar shows only `periodicTable`:
483
+ `graph` is blocked, and `protractor`, `lineReader` and `ruler` are outside the
484
+ allow-list. The item and passage placements show every tool they list.
593
485
 
594
- The runtime still distinguishes between contextual (`item`/`passage`) and section-wide tools:
486
+ ### Scope and Lifecycle
595
487
 
596
- Tool instances use structured IDs so scope is explicit:
488
+ Tool instances use structured ids that make their scope explicit:
597
489
 
598
490
  ```text
599
- <toolId>:<scopeLevel>:<scopeId>[:inline]
491
+ <toolId>:<scopeLevel>:<scopeId>
600
492
  ```
601
493
 
602
- Examples:
603
- - `calculator:section:section-1`
604
- - `calculator:item:item-42`
494
+ `scopeLevel` is one of `assessment`, `section`, `item`, `passage` or `rubric`,
495
+ and `createScopedToolId` throws on any other. The toolbars the players render
496
+ scope their ids at `section`, `item` or `passage`, and a rubric block's toolbar
497
+ scopes at `passage`. Examples from a section player:
498
+
499
+ - `lineReader:section:reading-1`
500
+ - `theme:section:reading-1`
501
+ - `calculator:item:q1`
605
502
  - `textToSpeech:passage:passage-1`
606
- - `highlighter:rubric:rubric-3`
503
+
504
+ The section player keeps every item card of a section mounted while the student
505
+ moves between its items. The ElementToolStateStore keeps answer eliminations,
506
+ annotation highlights and the color-scheme choice, and the coordinator's
507
+ `saveToolState`/`loadToolState` hooks carry them across reloads
508
+ ([Persistence Integration](#persistence-integration)). Which tools are open is
509
+ not restored, and graph points start over.
607
510
 
608
511
  ### Item-Level Tools (`tools.placement.item`)
609
512
 
610
- Tools that operate **within the context of a specific question/item**:
513
+ Tools that operate within the context of one item:
611
514
 
612
515
  ```typescript
613
516
  tools: {
@@ -618,24 +521,37 @@ tools: {
618
521
  ```
619
522
 
620
523
  **Characteristics:**
621
- - **Scope**: Bound to a specific item's DOM context
622
- - **Lifecycle**: Instance created/destroyed as you navigate between items
623
- - **State**: Isolated per-item (eliminations for Q5 don't affect Q6)
624
- - **UI Pattern**: Inline buttons in question headers/toolbars
625
- - **State Persistence**: Tracked per-item in ElementToolStateStore
626
-
627
- **Available Item-Level Tools:**
628
- - **TTS (Text-to-Speech)**: Reads the specific question/passage text
629
- - **Answer Eliminator**: Strikes through answer choices for that question
630
- - **Calculator**: Basic, scientific or graphing calculator
631
- - **Highlighter** (`annotationToolbar`): Highlights and annotates selected text; it opens from a text selection, outside the toolbars
524
+ - **Scope**: bound to one item's DOM context
525
+ - **Lifetime**: an item's tools live as long as its card, which stays mounted
526
+ until the section changes
527
+ - **State**: isolated per item (eliminations on Q5 don't affect Q6)
528
+ - **UI pattern**: buttons in the item toolbar; tools such as the calculator and
529
+ the dictionaries open in a floating panel
530
+ - **Persistence**: answer eliminations and annotation highlights are kept per
531
+ PIE element in the ElementToolStateStore
532
+
533
+ **Available item-level tools:**
534
+ - **Text-to-speech** (`textToSpeech`): reads the item or passage text
535
+ - **Answer eliminator** (`answerEliminator`, item only): strikes through answer
536
+ choices
537
+ - **Calculator** (`calculator`, item only): basic, scientific or graphing
538
+ calculator
539
+ - **Annotation toolbar** (`annotationToolbar`): highlights and annotates
540
+ selected text; it opens from a text selection, outside the toolbars
541
+ - **Line reader** (`lineReader`), **ruler** (`ruler`), **protractor**
542
+ (`protractor`), **graph** (`graph`) and **periodic table** (`periodicTable`)
543
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
544
+ `pictureDictionarySpanish`)
545
+
546
+ [Default Tools](docs/TOOL_REGISTRY.md#default-tools) lists every packaged tool
547
+ with the content it applies to.
632
548
 
633
549
  **Example Use Case:**
634
550
  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.
635
551
 
636
552
  ### Section-Level Tools (`tools.placement.section`)
637
553
 
638
- Tools that **float above the entire assessment** and persist across questions:
554
+ Tools that float above the section and stay open across its items:
639
555
 
640
556
  ```typescript
641
557
  tools: {
@@ -645,26 +561,30 @@ tools: {
645
561
  }
646
562
  ```
647
563
 
648
- The section player renders the section toolbar only when its `show-toolbar`
649
- attribute is `true`; the attribute defaults to `false`.
650
-
651
564
  **Characteristics:**
652
- - **Scope**: Section-wide, shared across all questions
653
- - **Lifecycle**: Single instance initialized for entire section
654
- - **State**: Persistent (a tool stays open and in place as you navigate)
655
- - **UI Pattern**: Draggable floating panels/overlays with z-index management
656
- - **State Persistence**: Global state maintained throughout section
657
-
658
- **Available Floating Tools:**
659
- - **Graph**: Coordinate plane
660
- - **Periodic Table**: Interactive periodic table reference
661
- - **Protractor**: Angle measurement tool
662
- - **Ruler**: Linear measurement tool (metric/imperial)
663
- - **Line Reader**: Reading guide/masking overlay
664
- - **Color Scheme** (`theme`): High-contrast color adjustments
565
+ - **Scope**: section-wide, shared across the section's items
566
+ - **Lifetime**: one instance per section; at a section change the tool
567
+ re-scopes to the new section and its state starts over
568
+ - **State**: an open tool stays open and in place while the student moves
569
+ between the section's items
570
+ - **UI pattern**: draggable floating panels with z-index management
571
+
572
+ **Available section-level tools:**
573
+ - **Graph** (`graph`): coordinate plane
574
+ - **Periodic table** (`periodicTable`): interactive periodic table reference
575
+ - **Protractor** (`protractor`): angle measurement
576
+ - **Ruler** (`ruler`): linear measurement (metric and imperial)
577
+ - **Line reader** (`lineReader`): reading guide and masking overlay
578
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
579
+ `pictureDictionarySpanish`)
580
+ - **The color-scheme tool** (`theme`): high-contrast color schemes; it also
581
+ supports assessment level
582
+
583
+ Every section-level tool except the color-scheme tool also supports item
584
+ placement.
665
585
 
666
586
  **Example Use Case:**
667
- A student opens the periodic table on Question 2 and moves it beside the passage. When they navigate to Question 7, it is still open where they left it.
587
+ 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.
668
588
 
669
589
  ### When to Use Each
670
590
 
@@ -674,25 +594,23 @@ Use **item-level tools** when:
674
594
  - Tool appears inline with the question (space-efficient)
675
595
  - Tool behavior is contextual to the current item
676
596
 
677
- Use **floating tools** when:
597
+ Use **section-level tools** when:
678
598
  - Tool is a general-purpose utility used across multiple questions
679
- - State should persist across navigation
599
+ - State should persist while the student moves between a section's items
680
600
  - Tool needs independent positioning and sizing
681
601
  - Tool provides reference information or computation capability
682
602
 
683
- ### Configuration Example
603
+ ### Configuration example
684
604
 
685
- Complete example showing both types:
605
+ A coordinator shows a tool only where `tools.placement` lists it, and its
606
+ default placement is empty. This configuration places the commonly used tools
607
+ at both kinds of level:
686
608
 
687
609
  ```typescript
688
- import {
689
- createPackagedToolRegistry,
690
- DEFAULT_TOOL_MODULE_LOADERS,
691
- } from '@pie-players/pie-default-tool-loaders';
610
+ import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
611
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
692
612
 
693
- const toolRegistry = createPackagedToolRegistry({
694
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
695
- });
613
+ const toolRegistry = createPackagedToolRegistry();
696
614
  const coordinator = new ToolkitCoordinator({
697
615
  assessmentId: 'math-exam',
698
616
  toolRegistry,
@@ -726,52 +644,16 @@ const coordinator = new ToolkitCoordinator({
726
644
  });
727
645
  ```
728
646
 
729
- **A common placement:**
647
+ ### Calculator providers
730
648
 
731
- A coordinator shows a tool only where `tools.placement` lists it. Its default
732
- placement is empty, so a coordinator configured without one shows no tools, and
733
- a profile grant does not place a tool either. This placement covers the
734
- commonly used tools:
735
-
736
- ```typescript
737
- import {
738
- createPackagedToolRegistry,
739
- DEFAULT_TOOL_MODULE_LOADERS,
740
- } from '@pie-players/pie-default-tool-loaders';
741
-
742
- const toolRegistry = createPackagedToolRegistry({
743
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
744
- });
745
- const coordinator = new ToolkitCoordinator({
746
- assessmentId: 'my-assessment',
747
- toolRegistry,
748
- tools: {
749
- placement: {
750
- section: ['graph', 'periodicTable', 'protractor', 'lineReader', 'ruler'],
751
- item: ['calculator', 'textToSpeech', 'answerEliminator'],
752
- passage: ['textToSpeech']
753
- },
754
- providers: {
755
- calculator: {
756
- enabled: true,
757
- provider: {
758
- runtime: { authFetcher: fetchLicensedDesmosApiKey }
759
- }
760
- },
761
- textToSpeech: { enabled: true, backend: 'browser' }
762
- }
763
- }
764
- });
765
- ```
766
-
767
- The ToolkitCoordinator handles service initialization, provider management, and
768
- state coordination. With no calculator provider configuration, the Desmos
769
- implementation remains the default. Desmos's CDN rejects a `calculator.js`
770
- request without an `apiKey` (HTTP 403), so the Desmos calculator opens only when
771
- the host supplies `provider.init.apiKey` or `provider.runtime.authFetcher`, or
772
- has already loaded `window.Desmos`; the adapter's keyless request grants no
773
- Desmos license either. Runtime key delivery avoids committing the key but does
774
- not hide it from the browser's Desmos script request. The bundled open-source
649
+ With no calculator provider configured, the calculator is Desmos. Desmos's CDN
650
+ rejects a `calculator.js` request without an `apiKey` (HTTP 403), so the Desmos
651
+ calculator opens only when the host supplies a key through
652
+ `provider.runtime.authFetcher`, a `provider.init.proxyEndpoint` that returns
653
+ `{ apiKey }`, or `provider.init.apiKey`, or has already loaded `window.Desmos`.
654
+ Without one the adapter throws "An apiKey or proxyEndpoint is required to load
655
+ Desmos." Runtime key delivery keeps the key out of the bundle, and the
656
+ browser's Desmos script request still carries it. The bundled open-source
775
657
  implementation below needs no key.
776
658
 
777
659
  Select the separate GeoGebra implementation explicitly without changing the
@@ -819,8 +701,9 @@ tools: {
819
701
  By default, server-backed TTS resolves:
820
702
 
821
703
  - `apiEndpoint: '/api/tts'`
822
- - `transportMode: 'pie'`
823
- - `endpointValidationMode: 'voices'`
704
+ - `transportMode: 'custom'` for `serverProvider: 'custom'`, else `'pie'`
705
+ - `endpointValidationMode: 'voices'`, so the provider reads the voices route
706
+ before it reports ready; `'none'` skips the probe
824
707
 
825
708
  You can still set `apiEndpoint` explicitly when your host route is not `/api/tts`.
826
709
 
@@ -957,9 +840,9 @@ Recommended host boundary:
957
840
  upstream.
958
841
  - Browser never receives shared secret, API key, or signing material.
959
842
 
960
- SchoolCity is used as a host-configured integration example for custom transport.
961
- Toolkit defaults still remain browser/standard providers unless the host explicitly
962
- configures custom server-backed TTS.
843
+ The section demos' `/api/tts/sc` route, backed by the SC adapter
844
+ (`@pie-players/tts-server-sc`), is the worked example of custom transport. TTS
845
+ stays on browser speech unless the host configures a server backend.
963
846
 
964
847
  ## Test Attempt Session
965
848
 
@@ -994,7 +877,7 @@ const itemSessions = toItemSessionsRecord(testAttemptSession);
994
877
  ### Integration Boundary
995
878
 
996
879
  - `@pie-players/pie-section-player` stays backend-agnostic and emits session/state changes.
997
- - Host applications own backend I/O to pie backend (`../../kds/pie-api-aws`).
880
+ - Host applications own backend I/O.
998
881
  - Hosts decide persistence policy (immediate, debounced, checkpoint, submit).
999
882
 
1000
883
  ### Section session API (controller + persistence)
@@ -1006,34 +889,6 @@ For section-level session flows, the toolkit supports two complementary APIs:
1006
889
 
1007
890
  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.
1008
891
 
1009
- ## Implementation Status
1010
-
1011
- ### ✅ Toolkit Services
1012
-
1013
- - **ToolkitCoordinator**: ⭐ NEW - Centralized service orchestration
1014
- - **ElementToolStateStore**: ⭐ NEW - Element-level ephemeral tool state management
1015
- - **ToolRegistry**: ⭐ NEW - Registry-based tool management with AfA PNP 3.0 profile support
1016
- - **ToolPolicyEngine**: AfA PNP 3.0 Personal Needs Profile and host policy decisions via registry-backed policy sources
1017
- - **ToolCoordinator**: Manages z-index layering and visibility for floating tools
1018
- - **HighlightCoordinator**: Separate highlight layers for TTS (temporary) and annotations (persistent)
1019
- - **TTSService**: Text-to-speech with QTI 3.0 catalog support
1020
- - **AccessibilityCatalogResolver**: QTI 3.0 accessibility catalog management
1021
- - **SSMLExtractor**: Automatic extraction of embedded `<speak>` tags
1022
-
1023
- ### ✅ PNP Support Ids
1024
-
1025
- - **Support ids are tool ids**: A profile, district policy or item setting names a tool by its `toolId`, and an id no registered tool carries raises `tool-policy.unknownSupportId`. [Support ids](docs/TOOL_REGISTRY.md#support-ids) lists the AfA PNP 3.0 terms the packaged ids serve.
1026
-
1027
- ### ✅ Section Player Integration
1028
-
1029
- The toolkit integrates seamlessly with the **PIE Section Player**:
1030
-
1031
- - **Primary Interface**: Section player is the main integration point
1032
- - **Default Coordinator**: Creates ToolkitCoordinator automatically if not provided
1033
- - **Automatic SSML Extraction**: Extracts embedded `<speak>` tags from passages and items
1034
- - **Catalog Lifecycle**: Manages item-level catalogs automatically
1035
- - **Service Coordination**: All toolkit services work together automatically
1036
-
1037
892
  ## ToolkitCoordinator API
1038
893
 
1039
894
  ### Configuration
@@ -1051,13 +906,18 @@ export interface ToolkitCoordinatorConfig {
1051
906
  item?: string[];
1052
907
  passage?: string[];
1053
908
  };
909
+ pnpEnforcement?: 'on' | 'off'; // omitted: auto-detected from the assessment
1054
910
  providers?: {
911
+ // Abridged; TextToSpeechToolProviderConfig carries every option.
1055
912
  textToSpeech?: {
1056
913
  enabled?: boolean;
1057
914
  backend?: 'browser' | 'server';
1058
915
  serverProvider?: 'polly' | 'google' | 'custom';
916
+ apiEndpoint?: string;
1059
917
  defaultVoice?: string;
1060
918
  rate?: number;
919
+ speedOptions?: TTSSpeedOption[];
920
+ layoutMode?: 'reserved-row' | 'expanding-row' | 'floating-overlay' | 'left-aligned';
1061
921
  };
1062
922
  calculator?: {
1063
923
  enabled?: boolean;
@@ -1073,63 +933,108 @@ export interface ToolkitCoordinatorConfig {
1073
933
  };
1074
934
  };
1075
935
  };
936
+ toolConfigStrictness?: 'off' | 'warn' | 'error'; // default 'error'
1076
937
  toolRegistry?: ToolRegistry | null;
938
+ toolContextResolvers?: ToolContextResolverMap;
1077
939
  accessibility?: {
1078
940
  catalogs?: any[];
1079
941
  language?: string;
1080
942
  };
943
+ hooks?: ToolkitCoordinatorHooks;
944
+ lazyInit?: boolean; // default false
945
+ eagerInit?: boolean; // default !lazyInit
1081
946
  }
1082
947
  ```
1083
948
 
949
+ A `ToolkitCoordinator` registers tool providers only from its `toolRegistry`.
950
+ Built without one, it adopts the registry of the toolkit it is bound to, such as
951
+ the section player's, which then validates its config and registers its
952
+ providers. A registry passed at construction is never replaced. Bound to a
953
+ toolkit that has none, it registers no providers, skips tool-id and placement
954
+ validation, and warns once (`tools.registryUnavailable`).
955
+
1084
956
  ### Methods
1085
957
 
1086
958
  ```typescript
1087
959
  // Tool configuration
1088
- coordinator.getToolConfig('textToSpeech'); // Get tool-specific config
1089
- coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // Update tool config; replaces the tool's provider, which starts again on next use
1090
-
1091
- // Final teardown by the owner that constructed the coordinator
1092
- await coordinator.dispose();
960
+ coordinator.getToolConfig('textToSpeech');
961
+ coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // replaces the tool's provider, which starts again on next use
962
+ coordinator.updateToolsPlacement({ item: ['calculator', 'textToSpeech'] });
963
+
964
+ // Tool policy
965
+ coordinator.decideToolPolicy({ level: 'item', scope: { level: 'item', scopeId: 'item-42' } });
966
+ coordinator.decideFeaturePolicy('transcript', { level: 'item', scopeId: 'item-42' });
967
+ const unregisterItem = coordinator.registerItemSettings('item-42', itemSettings);
968
+ const offPolicy = coordinator.onPolicyChange(listener);
969
+ const offDiagnostics = coordinator.onPolicyDiagnostic(listener);
970
+ coordinator.updateAssessment(assessment); // new profile or settings; null unbinds
971
+ coordinator.setPnpEnforcement('on'); // 'on' | 'off'; null returns to auto-detection
972
+ coordinator.getPolicyInputs(); // read-only inputs behind current decisions
973
+ const unregisterSource = coordinator.registerPolicySource(source);
974
+
975
+ // Tool requests: ask the toolbar hosting a tool to open it
976
+ const unregisterTarget = coordinator.registerToolRequestTarget(target);
977
+ coordinator.canRequestTool('calculator'); // false for a tool the deployment lacks
978
+ coordinator.requestTool({ toolId: 'calculator' }); // whether a toolbar claimed it; throws on an unknown id
979
+ const offTargets = coordinator.onToolRequestTargetsChange(listener);
980
+
981
+ // Providers and events
982
+ const provider = await coordinator.ensureProviderReady('textToSpeech');
983
+ const offErrors = coordinator.subscribeFrameworkErrors(listener);
984
+ const offCatalogs = coordinator.onCatalogsChange(listener);
985
+
986
+ // Readiness and teardown
987
+ await coordinator.waitUntilReady();
988
+ coordinator.isReady();
989
+ coordinator.getToolRegistry();
990
+ await coordinator.dispose(); // final teardown by the owner that constructed the coordinator
1093
991
  ```
1094
992
 
993
+ Each `register*`, `on*` and `subscribe*` call returns a function that undoes it.
994
+ The section-controller methods are under
995
+ [Controller Event Subscriptions](#controller-event-subscriptions-helper-first).
996
+
1095
997
  ### Direct Service Access
1096
998
 
1097
999
  All services are public properties for direct access:
1098
1000
 
1099
1001
  ```typescript
1100
1002
  coordinator.ttsService // TTSService instance
1101
- coordinator.toolCoordinator // ToolCoordinator instance
1003
+ coordinator.toolCoordinator // ToolCoordinatorApi, owned by the coordinator
1102
1004
  coordinator.highlightCoordinator // HighlightCoordinator instance
1103
1005
  coordinator.elementToolStateStore // ElementToolStateStore instance
1104
1006
  coordinator.catalogResolver // AccessibilityCatalogResolver instance
1007
+ coordinator.toolProviderRegistry // ToolProviderRegistry, the providers registered from toolRegistry
1105
1008
  ```
1106
1009
 
1107
1010
  ## ElementToolStateStore API
1108
1011
 
1109
- The `ElementToolStateStore` manages ephemeral tool state at the element level using globally unique composite keys.
1012
+ 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)).
1110
1013
 
1111
1014
  ### Key Concepts
1112
1015
 
1113
- - **Global Element ID**: Composite key format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`
1114
- - **Element-Level Granularity**: State tracked per PIE element (not per item)
1115
- - **Ephemeral State**: Tool state is client-only, separate from PIE session data
1016
+ - **Global Element ID**: Composite key format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`, with `attemptId` `""` when the host names no attempt
1017
+ - **Element-Level Granularity**: State tracked per PIE element; a tool keyed by item or section leaves `elementId`, and for a section `itemId`, as `""`
1018
+ - **Per-Attempt State**: two attempts at one section never share a key
1019
+ - **Separate from session data**: tool state never enters the PIE session and is never scored
1116
1020
  - **Cross-Section Persistence**: State persists when navigating between sections
1117
1021
 
1118
1022
  ### ID Utilities
1119
1023
 
1120
1024
  ```typescript
1121
1025
  // Generate global element ID
1122
- const globalElementId = store.getGlobalElementId(
1123
- 'demo-assessment',
1124
- 'section-1',
1125
- 'question-1',
1126
- 'mc1'
1127
- );
1128
- // Returns: "demo-assessment:section-1:question-1:mc1"
1026
+ const globalElementId = store.getGlobalElementId({
1027
+ assessmentId: 'demo-assessment',
1028
+ sectionId: 'section-1',
1029
+ attemptId: 'attempt-1',
1030
+ itemId: 'question-1',
1031
+ elementId: 'mc1'
1032
+ });
1033
+ // Returns: "demo-assessment:section-1:attempt-1:question-1:mc1"
1129
1034
 
1130
1035
  // Parse global element ID
1131
1036
  const components = store.parseGlobalElementId(globalElementId);
1132
- // Returns: { assessmentId, sectionId, itemId, elementId }
1037
+ // Returns: { assessmentId, sectionId, attemptId, itemId, elementId }
1133
1038
  ```
1134
1039
 
1135
1040
  ### CRUD Operations
@@ -1159,7 +1064,7 @@ store.clearElement(globalElementId);
1159
1064
  // Clear state for a specific tool across all elements
1160
1065
  store.clearTool('answerEliminator');
1161
1066
 
1162
- // Clear all elements in a specific section
1067
+ // Clear all elements in a specific section, across its attempts
1163
1068
  store.clearSection('demo-assessment', 'section-1');
1164
1069
 
1165
1070
  // Clear all state
@@ -1168,17 +1073,21 @@ store.clearAll();
1168
1073
 
1169
1074
  ### Persistence Integration
1170
1075
 
1076
+ A coordinator's store reports changes to the coordinator, which forwards them
1077
+ to `hooks.saveToolState`; `loadToolState` is read once while the coordinator
1078
+ gets ready. Persist tool state through those hooks. `store.setOnStateChange`
1079
+ holds a single callback, so calling it on a coordinator's store disconnects
1080
+ `saveToolState`.
1081
+
1171
1082
  ```typescript
1172
- // Set callback for persistence (e.g., localStorage)
1173
- store.setOnStateChange((state) => {
1174
- localStorage.setItem('tool-state', JSON.stringify(state));
1083
+ const coordinator = new ToolkitCoordinator({
1084
+ assessmentId: 'demo-assessment',
1085
+ toolRegistry,
1086
+ hooks: {
1087
+ loadToolState: () => JSON.parse(localStorage.getItem('tool-state') ?? 'null'),
1088
+ saveToolState: (state) => localStorage.setItem('tool-state', JSON.stringify(state)),
1089
+ },
1175
1090
  });
1176
-
1177
- // Load state from persistence
1178
- const saved = localStorage.getItem('tool-state');
1179
- if (saved) {
1180
- store.loadState(JSON.parse(saved));
1181
- }
1182
1091
  ```
1183
1092
 
1184
1093
  ### Reactivity
@@ -1252,7 +1161,8 @@ onDestroy(() => registration.release());
1252
1161
  Visibility and stacking then go through the coordinator by scoped tool id:
1253
1162
  `showTool`, `hideTool`, `toggleTool`, `isToolVisible`, `bringToFront(element)`,
1254
1163
  and `getVisibleTools({ baseId })` / `hideAllTools({ baseId })` for one tool
1255
- across scopes.
1164
+ across scopes. Disposing the toolkit coordinator releases every entry. Its
1165
+ debug lines print only under `window.PIE_DEBUG = true`.
1256
1166
 
1257
1167
  ### HighlightCoordinator
1258
1168
 
@@ -1282,12 +1192,14 @@ Reading highlights resolve through five `component-public` tokens registered in
1282
1192
  | `--pie-tts-word-underline` | `--pie-text` at 70% | Underline marking the current word |
1283
1193
  | `--pie-tts-word-shadow` | `--pie-text` at 35% | Shadow carrying the word over its fill |
1284
1194
 
1285
- The coordinator derives all five from the active theme's accent, text and
1286
- background — opacities clamped to a legible band, and the underline colour picked
1287
- by background luminance so the cue survives a dark scheme — then writes them
1288
- inline on the document element. Inline styles outrank any author selector, so a
1289
- host override takes `!important` and owns the contrast the derivation was
1290
- maintaining, across every scheme it ships.
1195
+ The coordinator derives all five from the active theme's `--pie-missing`,
1196
+ `--pie-text` and `--pie-background`. `--pie-missing` is the theme's
1197
+ missing-response feedback color, which reading highlights reuse as their accent.
1198
+ Opacities are clamped to a legible band, and the underline color is picked by
1199
+ background luminance so the cue survives a dark scheme. The coordinator writes
1200
+ the five inline on the document element. Inline styles outrank any author
1201
+ selector, so a host override takes `!important` and owns the contrast the
1202
+ derivation was maintaining, across every scheme it ships.
1291
1203
 
1292
1204
  ### AccessibilityCatalogResolver
1293
1205
 
@@ -1374,11 +1286,8 @@ catalogResolver.addItemCatalogs(result.catalogs);
1374
1286
 
1375
1287
  ### Cards without an extractor
1376
1288
 
1377
- Not every catalog card is lifted out of item markup. A signed alternate is
1378
- authored or written by an importer and has no extractor at all; one such lift
1379
- existed and was removed, because nothing produced the inline form and a runtime
1380
- that could not parse the markup left the video in the visible content, showing the
1381
- accommodation to every learner regardless of eligibility.
1289
+ A signed alternate is authored or written by an importer; no extractor lifts it
1290
+ from markup.
1382
1291
 
1383
1292
  This package resolves and registers those cards through
1384
1293
  `AccessibilityCatalogResolver` and the generic media helpers it re-exports
@@ -1391,25 +1300,18 @@ needs them — signing's card validators and its resolution rules live in
1391
1300
 
1392
1301
  `ToolPolicyEngine.decideFeature(featureId, scope?)` (and
1393
1302
  `ToolkitCoordinator.decideFeaturePolicy(featureId, scope?)`) resolve one feature id
1394
- through `PnpPolicySource`'s six-level precedence, independent of any toolbar
1395
- placement. Use it for capabilities that are not toolbar surfaces — signing is the
1396
- first — where a placement-scoped `decide(...)` would answer the wrong question:
1397
- absent because it was never placed, rather than absent because policy said no.
1398
- An item scope applies that item's registered settings, the item restriction and
1303
+ through `PnpPolicySource`'s PNP precedence, independent of any toolbar
1304
+ placement. Use it for capabilities that are not toolbar surfaces, such as
1305
+ signing: an item-level `decideToolPolicy(...)` also reports a tool absent when it
1306
+ was never placed, so it cannot tell a denial from a missing placement. An item
1307
+ scope applies that item's registered settings, the item restriction and
1399
1308
  requirement rungs; a decision without one applies no item's.
1400
1309
 
1401
- This package ships no profile. Which capabilities a deployment grants by default is a property
1402
- of the program rather than of a capability — TTS is a universal feature in one
1403
- program and a documented accommodation in another — so it belongs in policy
1404
- configuration alongside the district and test-administration levels. Hosts that
1405
- want today's universal set take `createUniversalPersonalNeedsProfile()` from
1406
- `@pie-players/pie-default-tool-loaders`, which ships it as data beside
1407
- `createEmptyPersonalNeedsProfile()`.
1310
+ This package ships no profile.
1311
+ [Universal and empty profiles](../default-tool-loaders/README.md#universal-and-empty-profiles)
1312
+ covers the profiles `@pie-players/pie-default-tool-loaders` builds.
1408
1313
 
1409
- Nothing derives a profile from the registry any more. Doing so read registry
1410
- membership as eligibility tier — registration means "policy-addressable", not
1411
- "universal, on by default" — and had to be corrected with a compile-time list of
1412
- ids to exclude that a host could not extend for its own accommodation.
1314
+ Registry membership makes a tool policy-addressable; it grants nothing.
1413
1315
 
1414
1316
  ### Live registry changes
1415
1317
 
@@ -1430,15 +1332,10 @@ The section player provides automatic ToolkitCoordinator integration:
1430
1332
 
1431
1333
  <script type="module">
1432
1334
  import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
1433
- import {
1434
- createPackagedToolRegistry,
1435
- DEFAULT_TOOL_MODULE_LOADERS,
1436
- } from '@pie-players/pie-default-tool-loaders';
1335
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
1437
1336
 
1438
1337
  // Create coordinator
1439
- const toolRegistry = createPackagedToolRegistry({
1440
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1441
- });
1338
+ const toolRegistry = createPackagedToolRegistry();
1442
1339
  const coordinator = new ToolkitCoordinator({
1443
1340
  assessmentId: 'my-assessment',
1444
1341
  toolRegistry,
@@ -1511,7 +1408,7 @@ player.section = mySection;
1511
1408
 
1512
1409
  // Internally creates:
1513
1410
  // new ToolkitCoordinator({
1514
- // assessmentId: 'section-demo-direct', // or runtime.assessmentId
1411
+ // assessmentId: runtime.assessmentId ?? section.identifier ?? 'assessment-<random>',
1515
1412
  // toolRegistry, // the player's toolRegistry, else the packaged registry
1516
1413
  // tools: player.runtime?.tools // no tools are placed when this is unset
1517
1414
  // })
@@ -1519,12 +1416,7 @@ player.section = mySection;
1519
1416
 
1520
1417
  ### Safe Custom Tool Configuration
1521
1418
 
1522
- Default behavior is now framework-owned: invalid tools/runtime initialization is handled in `pie-assessment-toolkit` without host try/catch.
1523
-
1524
- - Framework logs a deterministic console error prefix: `[pie-framework:<kind>:<source>]`
1525
- - Framework emits a canonical `framework-error` event
1526
- - Framework renders a fallback error panel instead of a blank player
1527
- - Startup tool-config validation can surface as `kind: "coordinator-init"` when the owned coordinator construction path throws.
1419
+ 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).
1528
1420
 
1529
1421
  Use `createToolsConfig()` when you want to pre-validate and inspect diagnostics before mounting:
1530
1422
 
@@ -1533,14 +1425,9 @@ import {
1533
1425
  createToolsConfig,
1534
1426
  ToolkitCoordinator
1535
1427
  } from "@pie-players/pie-assessment-toolkit";
1536
- import {
1537
- createPackagedToolRegistry,
1538
- DEFAULT_TOOL_MODULE_LOADERS,
1539
- } from "@pie-players/pie-default-tool-loaders";
1428
+ import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
1540
1429
 
1541
- const toolRegistry = createPackagedToolRegistry({
1542
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1543
- });
1430
+ const toolRegistry = createPackagedToolRegistry();
1544
1431
  const { config, diagnostics } = createToolsConfig({
1545
1432
  source: "host.bootstrap",
1546
1433
  strictness: "error",
@@ -1569,22 +1456,49 @@ Notes:
1569
1456
  - `providers.textToSpeech` is the canonical TTS provider key.
1570
1457
  - `providers.tts` is rejected by the validation contract.
1571
1458
  - A tool registration can declare `sanitizeConfig` and `validateConfig` hooks for its `providers.<toolId>` entry.
1572
- - Hosts can react to framework errors via the `framework-error` DOM event,
1573
- the `onFrameworkError(model)` callback prop, or by subscribing directly
1574
- to the package-internal bus via
1575
- `ToolkitCoordinator.subscribeFrameworkErrors(listener)`. The callback
1576
- prop fires exactly once per error, regardless of wrapper depth. Filter
1577
- by `model.kind` (e.g. `"tts-init"`, `"provider-init"`,
1578
- `"provider-register"`, `"tool-surface"`) for tool- or provider-specific
1579
- handling. Recoverable warnings remain observable but do not move section
1580
- readiness to `error`.
1581
- - See `docs/tools-and-accomodations/framework-owned-error-handling.md` for event payload and error-kind mapping details.
1459
+
1460
+ ## Instrumentation and Observability
1461
+
1462
+ Toolkit instrumentation is provider-agnostic. It uses the shared
1463
+ `InstrumentationProvider` contract from `@pie-players/pie-players-shared`.
1464
+
1465
+ ### Injection Path
1466
+
1467
+ When the toolkit is hosted by a section or assessment player, the provider
1468
+ comes from the item-player loader config:
1469
+
1470
+ - `runtime.player.loaderConfig.instrumentationProvider`
1471
+
1472
+ ### Semantics
1473
+
1474
+ - How an unset, `null` or invalid provider resolves is set out in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#provider-resolution).
1475
+ - Debug overlays can consume the same stream by composing providers with
1476
+ `CompositeInstrumentationProvider` (for example New Relic + debug panel).
1477
+ - Toolkit telemetry forwarding uses the same provider path, so tool/backend
1478
+ instrumentation is sent to production providers and is visible in debug panel
1479
+ overlays.
1480
+
1481
+ ### Toolkit-Owned Canonical Event Stream
1482
+
1483
+ - `pie-toolkit-runtime-owned`
1484
+ - `pie-toolkit-runtime-inherited`
1485
+ - `pie-toolkit-runtime-ready`
1486
+ - `pie-toolkit-ready`
1487
+ - `pie-toolkit-section-ready`
1488
+ - `pie-toolkit-framework-error`
1489
+
1490
+ Toolkit operational events are listed in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#operational-events).
1491
+
1492
+ The toolkit emits toolkit lifecycle events only; section and assessment events
1493
+ stay in their own layers, so no event has two emitters. The event bridge's
1494
+ optional `dedupeWindowMs` drops repeats within a window and is off by default.
1582
1495
 
1583
1496
  ## Section Runtime Engine (advanced)
1584
1497
 
1585
1498
  The section runtime engine is the section player's stage chain: a pure FSM that
1586
- turns the cohort a layout shows, the controller resolving and the readiness
1587
- signals the layout derives into `pie-stage-change` and `pie-loading-complete`.
1499
+ turns the cohort a layout shows (its `(sectionId, attemptId)` pair), the
1500
+ controller resolving and the readiness signals the layout derives into
1501
+ `pie-stage-change` and `pie-loading-complete`.
1588
1502
  The section player's layout kernel owns one per layout element, and it is the
1589
1503
  only stage emitter. `<pie-assessment-toolkit>` emits no stages: its
1590
1504
  `SectionControllerBinding` resolves the section's controller, which the toolkit
@@ -1611,14 +1525,16 @@ before `interactive` ends the chain: the first stage the cohort did not reach is
1611
1525
  `failed` and the rest up to `interactive` are `skipped`. The section player sets
1612
1526
  `runtimeError` from a non-recoverable `framework-error`, so a host waiting on
1613
1527
  `engine-ready` or `interactive` always hears an answer. `pie-loading-complete`
1614
- fires once per cohort, when every item has loaded.
1528
+ fires once per cohort, on the condition that opens `interactive`: the section
1529
+ controller is ready and the element pre-warm for the current items has
1530
+ resolved. The items load after it; the controller's `section-loading-complete`
1531
+ marks them loaded.
1615
1532
 
1616
1533
  ### Common-host wiring example
1617
1534
 
1618
1535
  Most hosts never construct the engine: the section-player layout elements do,
1619
1536
  and a host-built section layout takes its engine from
1620
- `pie-section-player-kernel-host` (see the section player's
1621
- [Custom layout authoring](../section-player/README.md#custom-layout-authoring)).
1537
+ `pie-section-player-kernel-host` ([custom section layouts](../../docs/section-player/custom-layouts.md)).
1622
1538
  Use the facade for a section renderer outside the section player. The shape
1623
1539
  mirrors the section-player kernel:
1624
1540
 
@@ -1695,21 +1611,23 @@ in the package's build with a pattern that covers subpaths, not a bare specifier
1695
1611
 
1696
1612
  `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1697
1613
  registration, a content resolver, its own custom element, and no edit to any
1698
- generic package. `packages/default-tool-loaders/README.md` covers how a
1699
- deployment then composes it in, and `docs/TOOL_REGISTRY.md` the registration and
1614
+ generic package. The [default-tool-loaders README](../default-tool-loaders/README.md)
1615
+ covers how a deployment then composes it in, and the
1616
+ [Tool Registry Reference](docs/TOOL_REGISTRY.md) the registration and
1700
1617
  host-surface contracts.
1701
1618
 
1702
- ## State Separation: Tool State vs Session Data
1619
+ ## State separation
1703
1620
 
1704
- The toolkit enforces a clear separation between ephemeral tool state and persistent session data:
1621
+ The toolkit keeps tool state and PIE session data in separate stores.
1705
1622
 
1706
- ### Tool State (Ephemeral - ElementToolStateStore)
1623
+ ### Tool state (ElementToolStateStore)
1707
1624
 
1708
- **Client-only**, never sent to server for scoring:
1625
+ Never scored; it leaves the browser only through the coordinator's
1626
+ `saveToolState` hook ([Persistence Integration](#persistence-integration)):
1709
1627
 
1710
1628
  ```typescript
1711
1629
  {
1712
- "demo-assessment:section-1:question-1:mc1": {
1630
+ "demo-assessment:section-1:attempt-1:question-1:mc1": {
1713
1631
  "answerEliminator": {
1714
1632
  "eliminatedChoices": ["choice-b", "choice-d"]
1715
1633
  },
@@ -1726,9 +1644,9 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1726
1644
  - Tool preferences
1727
1645
  - UI state
1728
1646
 
1729
- ### PIE Session Data (Persistent)
1647
+ ### PIE session data
1730
1648
 
1731
- **Sent to server** for scoring:
1649
+ The host persists it, and its backend scores it:
1732
1650
 
1733
1651
  ```typescript
1734
1652
  {
@@ -1748,11 +1666,7 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1748
1666
 
1749
1667
  ## Examples
1750
1668
 
1751
- See the [section-demos](../../apps/section-demos/) for complete examples:
1752
-
1753
- - **Three Questions Demo**: Element-level answer eliminator with state persistence
1754
- - **TTS Integration Demo**: Toolkit coordinator with TTS service
1755
- - **Paired Passages Demo**: Multi-section assessment with cross-section state
1669
+ 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".
1756
1670
 
1757
1671
  ## TypeScript Support
1758
1672
 
@@ -1776,31 +1690,27 @@ from tool configuration. Two sanitization layers apply:
1776
1690
  `pie-item-player`. See
1777
1691
  [pie-item-player README](../item-player/README.md#content-trust-boundary)
1778
1692
  for the `trust-markup` opt-out and the `sanitizeMarkup` override.
1779
- As a post-sanitization step, every authored `<img>` outside a `pie-*`
1780
- custom element is wrapped in `<span class="pie-image-scroll">` so
1781
- overwide images surface a horizontal scrollbar instead of being
1782
- clipped by the section layout's `overflow-x: hidden` ancestors
1783
- (PIE-94 / WCAG 1.4.10 Reflow at 400% zoom). The wrapper is
1784
- keyboard-scrollable (`tabindex="0"`, `role="region"`) and carries
1785
- the image's `alt` text in its `aria-label`; matching CSS lives in
1786
- `@pie-players/pie-theme` (`components.css`). Authored `<table>`
1787
- elements outside a `pie-*` custom element get the same treatment via
1788
- `<div class="pie-table-scroll">` so wide data grids reflow into a
1789
- horizontally scrollable region; the wrapper's `aria-label` is derived
1790
- from the table's `aria-label` / `aria-labelledby` / `<caption>`.
1693
+ The player also wraps overwide images and tables in scrollable regions
1694
+ ([reflow wrappers](../item-player/README.md#reflow-wrappers)).
1791
1695
  - **Tool icons and SSML** - tool-registered icon markup is parsed and
1792
1696
  DOMPurified inside the toolbar at render time; SSML payloads are
1793
1697
  restricted to an allow-listed subset of SSML tags/attributes before
1794
1698
  being forwarded to TTS providers. Do not ship tools that rely on raw
1795
1699
  `<script>` or event-handler attributes in their icon strings.
1796
1700
 
1701
+ [Security](../../docs/security/readme.md) sets out the full trust boundary and
1702
+ the host's obligations.
1703
+
1797
1704
  ## Related Documentation
1798
1705
 
1799
- - **[Tool Registry Architecture](docs/TOOL_REGISTRY.md)** - ⭐ NEW - Registry-based tool management and AfA PNP 3.0 profile support
1800
- - **[PNP Configuration Guide](docs/PNP_CONFIGURATION.md)** - ⭐ NEW - How to configure student profiles, district policies, and governance rules
1801
- - [ToolkitCoordinator Architecture](../../docs/architecture/architecture.md#toolkitcoordinator-centralized-service-management) - Design decisions and patterns
1706
+ - [Tool Registry Reference](docs/TOOL_REGISTRY.md) - Registry-based tool management and AfA PNP 3.0 profile support
1707
+ - [PNP Configuration Guide](docs/PNP_CONFIGURATION.md) - How to configure student profiles, district policies, and governance rules
1708
+ - [Configuring Tools](../../docs/tools-and-accomodations/tool_provider_system.md) - Placement, policy and provider configuration
1709
+ - [Toolkit Tool Host Contract](../../docs/tools-and-accomodations/tool_host_contract.md) - Runtime guarantees between hosts and toolkit-managed tools
1710
+ - [Tools and Accommodations Architecture](../../docs/tools-and-accomodations/architecture.md) - Tool design, scopes and activation
1711
+ - [Assessment toolkit in the architecture overview](../../docs/architecture/architecture.md#assessment-toolkit) - Where the toolkit sits among the players
1802
1712
  - [Section Player README](../section-player/README.md) - Section player integration
1803
- - [Section Player Architecture](../section-player/ARCHITECTURE.md#layered-runtime-engine-post-m7) - Layered runtime engine, kernel/toolkit wiring, lifecycle emit invariant
1713
+ - [Section Player Architecture](../section-player/ARCHITECTURE.md#layered-runtime-engine) - Layered runtime engine, kernel/toolkit wiring, lifecycle emit invariant
1804
1714
  - [Framework-Owned Error Handling](../../docs/tools-and-accomodations/framework-owned-error-handling.md) - Canonical framework error model/events and fallback behavior
1805
1715
  - [Safe Custom Tool Configuration](../../docs/tools-and-accomodations/safe-custom-tool-config.md) - Host-side config patterns and validation guidance
1806
1716
  - [Architecture Overview](../../docs/architecture/architecture.md) - Complete system architecture