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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +449 -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-9gqhfxtn.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 +30 -30
  23. package/dist/policy/core/decision-types.d.ts +44 -32
  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 +5 -2
  27. package/dist/policy/core/pnp-policy-inputs.js +12 -32
  28. package/dist/policy/core/provenance.d.ts +1 -1
  29. package/dist/policy/core/tool-parameters.d.ts +11 -0
  30. package/dist/policy/core/tool-parameters.js +15 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
  33. package/dist/policy/sources/PnpPolicySource.js +30 -28
  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 +49 -56
  66. package/dist/services/TTSService.js +308 -283
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +82 -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 +47 -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 +7 -23
  83. package/dist/services/tool-context.js +99 -53
  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 +1 -0
  98. package/dist/services/toolbar-decided-tools.js +1 -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 +4 -20
  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 +6 -5
  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
43
-
44
- await ttsService.initialize(new BrowserTTSProvider());
45
- ttsService.setCatalogResolver(catalogResolver);
46
-
47
- // Pass all services separately
48
- player.ttsService = ttsService;
49
- player.toolCoordinator = toolCoordinator;
50
- // ...
51
- ```
52
-
53
- **After** (coordinator orchestrates):
54
- ```typescript
55
- import {
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`
52
+ ## Capabilities
133
53
 
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.
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
137
67
 
138
- ## Architecture Overview
68
+ ## Architecture overview
139
69
 
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,107 @@ 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.
567
453
 
568
454
  Example:
569
455
 
570
456
  ```typescript
571
457
  tools: {
572
458
  policy: {
459
+ // Only these tools may appear...
573
460
  allowed: ['calculator', 'textToSpeech', 'answerEliminator', 'graph', 'periodicTable'],
461
+ // ...and graph never does.
574
462
  blocked: ['graph']
575
463
  },
576
464
  placement: {
@@ -589,25 +477,38 @@ tools: {
589
477
  }
590
478
  ```
591
479
 
592
- ### Scope and Lifecycle
480
+ With this configuration the section toolbar shows only `periodicTable`:
481
+ `graph` is blocked, and `protractor`, `lineReader` and `ruler` are outside the
482
+ allow-list. The item and passage placements show every tool they list.
593
483
 
594
- The runtime still distinguishes between contextual (`item`/`passage`) and section-wide tools:
484
+ ### Scope and Lifecycle
595
485
 
596
- Tool instances use structured IDs so scope is explicit:
486
+ Tool instances use structured ids that make their scope explicit:
597
487
 
598
488
  ```text
599
- <toolId>:<scopeLevel>:<scopeId>[:inline]
489
+ <toolId>:<scopeLevel>:<scopeId>
600
490
  ```
601
491
 
602
- Examples:
603
- - `calculator:section:section-1`
604
- - `calculator:item:item-42`
492
+ `scopeLevel` is one of `assessment`, `section`, `item`, `passage` or `rubric`,
493
+ and `createScopedToolId` throws on any other. The toolbars the players render
494
+ scope their ids at `section`, `item` or `passage`, and a rubric block's toolbar
495
+ scopes at `passage`. Examples from a section player:
496
+
497
+ - `lineReader:section:reading-1`
498
+ - `theme:section:reading-1`
499
+ - `calculator:item:q1`
605
500
  - `textToSpeech:passage:passage-1`
606
- - `highlighter:rubric:rubric-3`
501
+
502
+ The section player keeps every item card of a section mounted while the student
503
+ moves between its items. The ElementToolStateStore keeps answer eliminations,
504
+ annotation highlights and the color-scheme choice, and the coordinator's
505
+ `saveToolState`/`loadToolState` hooks carry them across reloads
506
+ ([Persistence Integration](#persistence-integration)). Which tools are open is
507
+ not restored, and graph points start over.
607
508
 
608
509
  ### Item-Level Tools (`tools.placement.item`)
609
510
 
610
- Tools that operate **within the context of a specific question/item**:
511
+ Tools that operate within the context of one item:
611
512
 
612
513
  ```typescript
613
514
  tools: {
@@ -618,24 +519,37 @@ tools: {
618
519
  ```
619
520
 
620
521
  **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
522
+ - **Scope**: bound to one item's DOM context
523
+ - **Lifetime**: an item's tools live as long as its card, which stays mounted
524
+ until the section changes
525
+ - **State**: isolated per item (eliminations on Q5 don't affect Q6)
526
+ - **UI pattern**: buttons in the item toolbar; tools such as the calculator and
527
+ the dictionaries open in a floating panel
528
+ - **Persistence**: answer eliminations and annotation highlights are kept per
529
+ PIE element in the ElementToolStateStore
530
+
531
+ **Available item-level tools:**
532
+ - **Text-to-speech** (`textToSpeech`): reads the item or passage text
533
+ - **Answer eliminator** (`answerEliminator`, item only): strikes through answer
534
+ choices
535
+ - **Calculator** (`calculator`, item only): basic, scientific or graphing
536
+ calculator
537
+ - **Annotation toolbar** (`annotationToolbar`): highlights and annotates
538
+ selected text; it opens from a text selection, outside the toolbars
539
+ - **Line reader** (`lineReader`), **ruler** (`ruler`), **protractor**
540
+ (`protractor`), **graph** (`graph`) and **periodic table** (`periodicTable`)
541
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
542
+ `pictureDictionarySpanish`)
543
+
544
+ [Default Tools](docs/TOOL_REGISTRY.md#default-tools) lists every packaged tool
545
+ with the content it applies to.
632
546
 
633
547
  **Example Use Case:**
634
548
  A student uses answer eliminator on Question 3 to cross out choices B and D. When they navigate to Question 4, they see fresh, uneliminated choices. When they return to Question 3, their eliminations are restored.
635
549
 
636
550
  ### Section-Level Tools (`tools.placement.section`)
637
551
 
638
- Tools that **float above the entire assessment** and persist across questions:
552
+ Tools that float above the section and stay open across its items:
639
553
 
640
554
  ```typescript
641
555
  tools: {
@@ -645,26 +559,30 @@ tools: {
645
559
  }
646
560
  ```
647
561
 
648
- The section player renders the section toolbar only when its `show-toolbar`
649
- attribute is `true`; the attribute defaults to `false`.
650
-
651
562
  **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
563
+ - **Scope**: section-wide, shared across the section's items
564
+ - **Lifetime**: one instance per section; at a section change the tool
565
+ re-scopes to the new section and its state starts over
566
+ - **State**: an open tool stays open and in place while the student moves
567
+ between the section's items
568
+ - **UI pattern**: draggable floating panels with z-index management
569
+
570
+ **Available section-level tools:**
571
+ - **Graph** (`graph`): coordinate plane
572
+ - **Periodic table** (`periodicTable`): interactive periodic table reference
573
+ - **Protractor** (`protractor`): angle measurement
574
+ - **Ruler** (`ruler`): linear measurement (metric and imperial)
575
+ - **Line reader** (`lineReader`): reading guide and masking overlay
576
+ - **Dictionaries** (`dictionary`, `pictureDictionary`, `dictionarySpanish`,
577
+ `pictureDictionarySpanish`)
578
+ - **The color-scheme tool** (`theme`): high-contrast color schemes; it also
579
+ supports assessment level
580
+
581
+ Every section-level tool except the color-scheme tool also supports item
582
+ placement.
665
583
 
666
584
  **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.
585
+ A student opens the periodic table on Question 2 and moves it beside the passage. When they navigate to Question 7 of the same section, it is still open where they left it.
668
586
 
669
587
  ### When to Use Each
670
588
 
@@ -674,25 +592,23 @@ Use **item-level tools** when:
674
592
  - Tool appears inline with the question (space-efficient)
675
593
  - Tool behavior is contextual to the current item
676
594
 
677
- Use **floating tools** when:
595
+ Use **section-level tools** when:
678
596
  - Tool is a general-purpose utility used across multiple questions
679
- - State should persist across navigation
597
+ - State should persist while the student moves between a section's items
680
598
  - Tool needs independent positioning and sizing
681
599
  - Tool provides reference information or computation capability
682
600
 
683
- ### Configuration Example
601
+ ### Configuration example
684
602
 
685
- Complete example showing both types:
603
+ A coordinator shows a tool only where `tools.placement` lists it, and its
604
+ default placement is empty. This configuration places the commonly used tools
605
+ at both kinds of level:
686
606
 
687
607
  ```typescript
688
- import {
689
- createPackagedToolRegistry,
690
- DEFAULT_TOOL_MODULE_LOADERS,
691
- } from '@pie-players/pie-default-tool-loaders';
608
+ import { ToolkitCoordinator } from '@pie-players/pie-assessment-toolkit';
609
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
692
610
 
693
- const toolRegistry = createPackagedToolRegistry({
694
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
695
- });
611
+ const toolRegistry = createPackagedToolRegistry();
696
612
  const coordinator = new ToolkitCoordinator({
697
613
  assessmentId: 'math-exam',
698
614
  toolRegistry,
@@ -726,52 +642,16 @@ const coordinator = new ToolkitCoordinator({
726
642
  });
727
643
  ```
728
644
 
729
- **A common placement:**
645
+ ### Calculator providers
730
646
 
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
647
+ With no calculator provider configured, the calculator is Desmos. Desmos's CDN
648
+ rejects a `calculator.js` request without an `apiKey` (HTTP 403), so the Desmos
649
+ calculator opens only when the host supplies a key through
650
+ `provider.runtime.authFetcher`, a `provider.init.proxyEndpoint` that returns
651
+ `{ apiKey }`, or `provider.init.apiKey`, or has already loaded `window.Desmos`.
652
+ Without one the adapter throws "An apiKey or proxyEndpoint is required to load
653
+ Desmos." Runtime key delivery keeps the key out of the bundle, and the
654
+ browser's Desmos script request still carries it. The bundled open-source
775
655
  implementation below needs no key.
776
656
 
777
657
  Select the separate GeoGebra implementation explicitly without changing the
@@ -819,8 +699,9 @@ tools: {
819
699
  By default, server-backed TTS resolves:
820
700
 
821
701
  - `apiEndpoint: '/api/tts'`
822
- - `transportMode: 'pie'`
823
- - `endpointValidationMode: 'voices'`
702
+ - `transportMode: 'custom'` for `serverProvider: 'custom'`, else `'pie'`
703
+ - `endpointValidationMode: 'voices'`, so the provider reads the voices route
704
+ before it reports ready; `'none'` skips the probe
824
705
 
825
706
  You can still set `apiEndpoint` explicitly when your host route is not `/api/tts`.
826
707
 
@@ -957,9 +838,9 @@ Recommended host boundary:
957
838
  upstream.
958
839
  - Browser never receives shared secret, API key, or signing material.
959
840
 
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.
841
+ The section demos' `/api/tts/sc` route, backed by the SC adapter
842
+ (`@pie-players/tts-server-sc`), is the worked example of custom transport. TTS
843
+ stays on browser speech unless the host configures a server backend.
963
844
 
964
845
  ## Test Attempt Session
965
846
 
@@ -994,7 +875,7 @@ const itemSessions = toItemSessionsRecord(testAttemptSession);
994
875
  ### Integration Boundary
995
876
 
996
877
  - `@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`).
878
+ - Host applications own backend I/O.
998
879
  - Hosts decide persistence policy (immediate, debounced, checkpoint, submit).
999
880
 
1000
881
  ### Section session API (controller + persistence)
@@ -1006,34 +887,6 @@ For section-level session flows, the toolkit supports two complementary APIs:
1006
887
 
1007
888
  The persistence strategy works with the same `SectionControllerSessionState` shape exposed by the controller, so hosts can choose bulk restore (`applySession`) and fine-grained updates (`updateItemSession`) without internal runtime coupling.
1008
889
 
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
890
  ## ToolkitCoordinator API
1038
891
 
1039
892
  ### Configuration
@@ -1051,13 +904,18 @@ export interface ToolkitCoordinatorConfig {
1051
904
  item?: string[];
1052
905
  passage?: string[];
1053
906
  };
907
+ pnpEnforcement?: 'on' | 'off'; // omitted: auto-detected from the assessment
1054
908
  providers?: {
909
+ // Abridged; TextToSpeechToolProviderConfig carries every option.
1055
910
  textToSpeech?: {
1056
911
  enabled?: boolean;
1057
912
  backend?: 'browser' | 'server';
1058
913
  serverProvider?: 'polly' | 'google' | 'custom';
914
+ apiEndpoint?: string;
1059
915
  defaultVoice?: string;
1060
916
  rate?: number;
917
+ speedOptions?: TTSSpeedOption[];
918
+ layoutMode?: 'reserved-row' | 'expanding-row' | 'floating-overlay' | 'left-aligned';
1061
919
  };
1062
920
  calculator?: {
1063
921
  enabled?: boolean;
@@ -1073,63 +931,108 @@ export interface ToolkitCoordinatorConfig {
1073
931
  };
1074
932
  };
1075
933
  };
934
+ toolConfigStrictness?: 'off' | 'warn' | 'error'; // default 'error'
1076
935
  toolRegistry?: ToolRegistry | null;
936
+ toolContextResolvers?: ToolContextResolverMap;
1077
937
  accessibility?: {
1078
938
  catalogs?: any[];
1079
939
  language?: string;
1080
940
  };
941
+ hooks?: ToolkitCoordinatorHooks;
942
+ lazyInit?: boolean; // default false
943
+ eagerInit?: boolean; // default !lazyInit
1081
944
  }
1082
945
  ```
1083
946
 
947
+ A `ToolkitCoordinator` registers tool providers only from its `toolRegistry`.
948
+ Built without one, it adopts the registry of the toolkit it is bound to, such as
949
+ the section player's, which then validates its config and registers its
950
+ providers. A registry passed at construction is never replaced. Bound to a
951
+ toolkit that has none, it registers no providers, skips tool-id and placement
952
+ validation, and warns once (`tools.registryUnavailable`).
953
+
1084
954
  ### Methods
1085
955
 
1086
956
  ```typescript
1087
957
  // 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();
958
+ coordinator.getToolConfig('textToSpeech');
959
+ coordinator.updateToolConfig('textToSpeech', { rate: 1.5 }); // replaces the tool's provider, which starts again on next use
960
+ coordinator.updateToolsPlacement({ item: ['calculator', 'textToSpeech'] });
961
+
962
+ // Tool policy
963
+ coordinator.decideToolPolicy({ level: 'item', scope: { level: 'item', scopeId: 'item-42' } });
964
+ coordinator.decideFeaturePolicy('transcript', { level: 'item', scopeId: 'item-42' });
965
+ const unregisterItem = coordinator.registerItemSettings('item-42', itemSettings);
966
+ const offPolicy = coordinator.onPolicyChange(listener);
967
+ const offDiagnostics = coordinator.onPolicyDiagnostic(listener);
968
+ coordinator.updateAssessment(assessment); // new profile or settings; null unbinds
969
+ coordinator.setPnpEnforcement('on'); // 'on' | 'off'; null returns to auto-detection
970
+ coordinator.getPolicyInputs(); // read-only inputs behind current decisions
971
+ const unregisterSource = coordinator.registerPolicySource(source);
972
+
973
+ // Tool requests: ask the toolbar hosting a tool to open it
974
+ const unregisterTarget = coordinator.registerToolRequestTarget(target);
975
+ coordinator.canRequestTool('calculator'); // false for a tool the deployment lacks
976
+ coordinator.requestTool({ toolId: 'calculator' }); // whether a toolbar claimed it; throws on an unknown id
977
+ const offTargets = coordinator.onToolRequestTargetsChange(listener);
978
+
979
+ // Providers and events
980
+ const provider = await coordinator.ensureProviderReady('textToSpeech');
981
+ const offErrors = coordinator.subscribeFrameworkErrors(listener);
982
+ const offCatalogs = coordinator.onCatalogsChange(listener);
983
+
984
+ // Readiness and teardown
985
+ await coordinator.waitUntilReady();
986
+ coordinator.isReady();
987
+ coordinator.getToolRegistry();
988
+ await coordinator.dispose(); // final teardown by the owner that constructed the coordinator
1093
989
  ```
1094
990
 
991
+ Each `register*`, `on*` and `subscribe*` call returns a function that undoes it.
992
+ The section-controller methods are under
993
+ [Controller Event Subscriptions](#controller-event-subscriptions-helper-first).
994
+
1095
995
  ### Direct Service Access
1096
996
 
1097
997
  All services are public properties for direct access:
1098
998
 
1099
999
  ```typescript
1100
1000
  coordinator.ttsService // TTSService instance
1101
- coordinator.toolCoordinator // ToolCoordinator instance
1001
+ coordinator.toolCoordinator // ToolCoordinatorApi, owned by the coordinator
1102
1002
  coordinator.highlightCoordinator // HighlightCoordinator instance
1103
1003
  coordinator.elementToolStateStore // ElementToolStateStore instance
1104
1004
  coordinator.catalogResolver // AccessibilityCatalogResolver instance
1005
+ coordinator.toolProviderRegistry // ToolProviderRegistry, the providers registered from toolRegistry
1105
1006
  ```
1106
1007
 
1107
1008
  ## ElementToolStateStore API
1108
1009
 
1109
- The `ElementToolStateStore` manages ephemeral tool state at the element level using globally unique composite keys.
1010
+ The `ElementToolStateStore` holds tool state per PIE element under globally unique composite keys. It is kept apart from PIE session data and leaves the browser only through the coordinator's `saveToolState` hook ([Persistence Integration](#persistence-integration)).
1110
1011
 
1111
1012
  ### Key Concepts
1112
1013
 
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
1014
+ - **Global Element ID**: Composite key format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`, with `attemptId` `""` when the host names no attempt
1015
+ - **Element-Level Granularity**: State tracked per PIE element; a tool keyed by item or section leaves `elementId`, and for a section `itemId`, as `""`
1016
+ - **Per-Attempt State**: two attempts at one section never share a key
1017
+ - **Separate from session data**: tool state never enters the PIE session and is never scored
1116
1018
  - **Cross-Section Persistence**: State persists when navigating between sections
1117
1019
 
1118
1020
  ### ID Utilities
1119
1021
 
1120
1022
  ```typescript
1121
1023
  // 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"
1024
+ const globalElementId = store.getGlobalElementId({
1025
+ assessmentId: 'demo-assessment',
1026
+ sectionId: 'section-1',
1027
+ attemptId: 'attempt-1',
1028
+ itemId: 'question-1',
1029
+ elementId: 'mc1'
1030
+ });
1031
+ // Returns: "demo-assessment:section-1:attempt-1:question-1:mc1"
1129
1032
 
1130
1033
  // Parse global element ID
1131
1034
  const components = store.parseGlobalElementId(globalElementId);
1132
- // Returns: { assessmentId, sectionId, itemId, elementId }
1035
+ // Returns: { assessmentId, sectionId, attemptId, itemId, elementId }
1133
1036
  ```
1134
1037
 
1135
1038
  ### CRUD Operations
@@ -1159,7 +1062,7 @@ store.clearElement(globalElementId);
1159
1062
  // Clear state for a specific tool across all elements
1160
1063
  store.clearTool('answerEliminator');
1161
1064
 
1162
- // Clear all elements in a specific section
1065
+ // Clear all elements in a specific section, across its attempts
1163
1066
  store.clearSection('demo-assessment', 'section-1');
1164
1067
 
1165
1068
  // Clear all state
@@ -1168,17 +1071,21 @@ store.clearAll();
1168
1071
 
1169
1072
  ### Persistence Integration
1170
1073
 
1074
+ A coordinator's store reports changes to the coordinator, which forwards them
1075
+ to `hooks.saveToolState`; `loadToolState` is read once while the coordinator
1076
+ gets ready. Persist tool state through those hooks. `store.setOnStateChange`
1077
+ holds a single callback, so calling it on a coordinator's store disconnects
1078
+ `saveToolState`.
1079
+
1171
1080
  ```typescript
1172
- // Set callback for persistence (e.g., localStorage)
1173
- store.setOnStateChange((state) => {
1174
- localStorage.setItem('tool-state', JSON.stringify(state));
1081
+ const coordinator = new ToolkitCoordinator({
1082
+ assessmentId: 'demo-assessment',
1083
+ toolRegistry,
1084
+ hooks: {
1085
+ loadToolState: () => JSON.parse(localStorage.getItem('tool-state') ?? 'null'),
1086
+ saveToolState: (state) => localStorage.setItem('tool-state', JSON.stringify(state)),
1087
+ },
1175
1088
  });
1176
-
1177
- // Load state from persistence
1178
- const saved = localStorage.getItem('tool-state');
1179
- if (saved) {
1180
- store.loadState(JSON.parse(saved));
1181
- }
1182
1089
  ```
1183
1090
 
1184
1091
  ### Reactivity
@@ -1252,7 +1159,8 @@ onDestroy(() => registration.release());
1252
1159
  Visibility and stacking then go through the coordinator by scoped tool id:
1253
1160
  `showTool`, `hideTool`, `toggleTool`, `isToolVisible`, `bringToFront(element)`,
1254
1161
  and `getVisibleTools({ baseId })` / `hideAllTools({ baseId })` for one tool
1255
- across scopes.
1162
+ across scopes. Disposing the toolkit coordinator releases every entry. Its
1163
+ debug lines print only under `window.PIE_DEBUG = true`.
1256
1164
 
1257
1165
  ### HighlightCoordinator
1258
1166
 
@@ -1282,12 +1190,14 @@ Reading highlights resolve through five `component-public` tokens registered in
1282
1190
  | `--pie-tts-word-underline` | `--pie-text` at 70% | Underline marking the current word |
1283
1191
  | `--pie-tts-word-shadow` | `--pie-text` at 35% | Shadow carrying the word over its fill |
1284
1192
 
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.
1193
+ The coordinator derives all five from the active theme's `--pie-missing`,
1194
+ `--pie-text` and `--pie-background`. `--pie-missing` is the theme's
1195
+ missing-response feedback color, which reading highlights reuse as their accent.
1196
+ Opacities are clamped to a legible band, and the underline color is picked by
1197
+ background luminance so the cue survives a dark scheme. The coordinator writes
1198
+ the five inline on the document element. Inline styles outrank any author
1199
+ selector, so a host override takes `!important` and owns the contrast the
1200
+ derivation was maintaining, across every scheme it ships.
1291
1201
 
1292
1202
  ### AccessibilityCatalogResolver
1293
1203
 
@@ -1374,11 +1284,8 @@ catalogResolver.addItemCatalogs(result.catalogs);
1374
1284
 
1375
1285
  ### Cards without an extractor
1376
1286
 
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.
1287
+ A signed alternate is authored or written by an importer; no extractor lifts it
1288
+ from markup.
1382
1289
 
1383
1290
  This package resolves and registers those cards through
1384
1291
  `AccessibilityCatalogResolver` and the generic media helpers it re-exports
@@ -1391,25 +1298,18 @@ needs them — signing's card validators and its resolution rules live in
1391
1298
 
1392
1299
  `ToolPolicyEngine.decideFeature(featureId, scope?)` (and
1393
1300
  `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
1301
+ through `PnpPolicySource`'s PNP precedence, independent of any toolbar
1302
+ placement. Use it for capabilities that are not toolbar surfaces, such as
1303
+ signing: an item-level `decideToolPolicy(...)` also reports a tool absent when it
1304
+ was never placed, so it cannot tell a denial from a missing placement. An item
1305
+ scope applies that item's registered settings, the item restriction and
1399
1306
  requirement rungs; a decision without one applies no item's.
1400
1307
 
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()`.
1308
+ This package ships no profile.
1309
+ [Universal and empty profiles](../default-tool-loaders/README.md#universal-and-empty-profiles)
1310
+ covers the profiles `@pie-players/pie-default-tool-loaders` builds.
1408
1311
 
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.
1312
+ Registry membership makes a tool policy-addressable; it grants nothing.
1413
1313
 
1414
1314
  ### Live registry changes
1415
1315
 
@@ -1430,15 +1330,10 @@ The section player provides automatic ToolkitCoordinator integration:
1430
1330
 
1431
1331
  <script type="module">
1432
1332
  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';
1333
+ import { createPackagedToolRegistry } from '@pie-players/pie-default-tool-loaders';
1437
1334
 
1438
1335
  // Create coordinator
1439
- const toolRegistry = createPackagedToolRegistry({
1440
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1441
- });
1336
+ const toolRegistry = createPackagedToolRegistry();
1442
1337
  const coordinator = new ToolkitCoordinator({
1443
1338
  assessmentId: 'my-assessment',
1444
1339
  toolRegistry,
@@ -1511,7 +1406,7 @@ player.section = mySection;
1511
1406
 
1512
1407
  // Internally creates:
1513
1408
  // new ToolkitCoordinator({
1514
- // assessmentId: 'section-demo-direct', // or runtime.assessmentId
1409
+ // assessmentId: runtime.assessmentId ?? section.identifier ?? 'assessment-<random>',
1515
1410
  // toolRegistry, // the player's toolRegistry, else the packaged registry
1516
1411
  // tools: player.runtime?.tools // no tools are placed when this is unset
1517
1412
  // })
@@ -1519,12 +1414,7 @@ player.section = mySection;
1519
1414
 
1520
1415
  ### Safe Custom Tool Configuration
1521
1416
 
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.
1417
+ Invalid tool or runtime initialization is handled inside `pie-assessment-toolkit`; the host needs no try/catch. The error model, its events and hooks, and the fallback panel are in [Framework-owned error handling](../../docs/tools-and-accomodations/framework-owned-error-handling.md); host-side config patterns are in [Safe custom tool configuration](../../docs/tools-and-accomodations/safe-custom-tool-config.md).
1528
1418
 
1529
1419
  Use `createToolsConfig()` when you want to pre-validate and inspect diagnostics before mounting:
1530
1420
 
@@ -1533,14 +1423,9 @@ import {
1533
1423
  createToolsConfig,
1534
1424
  ToolkitCoordinator
1535
1425
  } from "@pie-players/pie-assessment-toolkit";
1536
- import {
1537
- createPackagedToolRegistry,
1538
- DEFAULT_TOOL_MODULE_LOADERS,
1539
- } from "@pie-players/pie-default-tool-loaders";
1426
+ import { createPackagedToolRegistry } from "@pie-players/pie-default-tool-loaders";
1540
1427
 
1541
- const toolRegistry = createPackagedToolRegistry({
1542
- toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
1543
- });
1428
+ const toolRegistry = createPackagedToolRegistry();
1544
1429
  const { config, diagnostics } = createToolsConfig({
1545
1430
  source: "host.bootstrap",
1546
1431
  strictness: "error",
@@ -1569,22 +1454,49 @@ Notes:
1569
1454
  - `providers.textToSpeech` is the canonical TTS provider key.
1570
1455
  - `providers.tts` is rejected by the validation contract.
1571
1456
  - 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.
1457
+
1458
+ ## Instrumentation and Observability
1459
+
1460
+ Toolkit instrumentation is provider-agnostic. It uses the shared
1461
+ `InstrumentationProvider` contract from `@pie-players/pie-players-shared`.
1462
+
1463
+ ### Injection Path
1464
+
1465
+ When the toolkit is hosted by a section or assessment player, the provider
1466
+ comes from the item-player loader config:
1467
+
1468
+ - `runtime.player.loaderConfig.instrumentationProvider`
1469
+
1470
+ ### Semantics
1471
+
1472
+ - How an unset, `null` or invalid provider resolves is set out in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#provider-resolution).
1473
+ - Debug overlays can consume the same stream by composing providers with
1474
+ `CompositeInstrumentationProvider` (for example New Relic + debug panel).
1475
+ - Toolkit telemetry forwarding uses the same provider path, so tool/backend
1476
+ instrumentation is sent to production providers and is visible in debug panel
1477
+ overlays.
1478
+
1479
+ ### Toolkit-Owned Canonical Event Stream
1480
+
1481
+ - `pie-toolkit-runtime-owned`
1482
+ - `pie-toolkit-runtime-inherited`
1483
+ - `pie-toolkit-runtime-ready`
1484
+ - `pie-toolkit-ready`
1485
+ - `pie-toolkit-section-ready`
1486
+ - `pie-toolkit-framework-error`
1487
+
1488
+ Toolkit operational events are listed in [Instrumentation providers](../../docs/architecture/instrumentation-providers.md#operational-events).
1489
+
1490
+ The toolkit emits toolkit lifecycle events only; section and assessment events
1491
+ stay in their own layers, so no event has two emitters. The event bridge's
1492
+ optional `dedupeWindowMs` drops repeats within a window and is off by default.
1582
1493
 
1583
1494
  ## Section Runtime Engine (advanced)
1584
1495
 
1585
1496
  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`.
1497
+ turns the cohort a layout shows (its `(sectionId, attemptId)` pair), the
1498
+ controller resolving and the readiness signals the layout derives into
1499
+ `pie-stage-change` and `pie-loading-complete`.
1588
1500
  The section player's layout kernel owns one per layout element, and it is the
1589
1501
  only stage emitter. `<pie-assessment-toolkit>` emits no stages: its
1590
1502
  `SectionControllerBinding` resolves the section's controller, which the toolkit
@@ -1611,14 +1523,16 @@ before `interactive` ends the chain: the first stage the cohort did not reach is
1611
1523
  `failed` and the rest up to `interactive` are `skipped`. The section player sets
1612
1524
  `runtimeError` from a non-recoverable `framework-error`, so a host waiting on
1613
1525
  `engine-ready` or `interactive` always hears an answer. `pie-loading-complete`
1614
- fires once per cohort, when every item has loaded.
1526
+ fires once per cohort, on the condition that opens `interactive`: the section
1527
+ controller is ready and the element pre-warm for the current items has
1528
+ resolved. The items load after it; the controller's `section-loading-complete`
1529
+ marks them loaded.
1615
1530
 
1616
1531
  ### Common-host wiring example
1617
1532
 
1618
1533
  Most hosts never construct the engine: the section-player layout elements do,
1619
1534
  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)).
1535
+ `pie-section-player-kernel-host` ([custom section layouts](../../docs/section-player/custom-layouts.md)).
1622
1536
  Use the facade for a section renderer outside the section player. The shape
1623
1537
  mirrors the section-player kernel:
1624
1538
 
@@ -1695,21 +1609,23 @@ in the package's build with a pattern that covers subpaths, not a bare specifier
1695
1609
 
1696
1610
  `@pie-players/pie-tool-sign-language` is the worked example end to end: a
1697
1611
  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
1612
+ generic package. The [default-tool-loaders README](../default-tool-loaders/README.md)
1613
+ covers how a deployment then composes it in, and the
1614
+ [Tool Registry Reference](docs/TOOL_REGISTRY.md) the registration and
1700
1615
  host-surface contracts.
1701
1616
 
1702
- ## State Separation: Tool State vs Session Data
1617
+ ## State separation
1703
1618
 
1704
- The toolkit enforces a clear separation between ephemeral tool state and persistent session data:
1619
+ The toolkit keeps tool state and PIE session data in separate stores.
1705
1620
 
1706
- ### Tool State (Ephemeral - ElementToolStateStore)
1621
+ ### Tool state (ElementToolStateStore)
1707
1622
 
1708
- **Client-only**, never sent to server for scoring:
1623
+ Never scored; it leaves the browser only through the coordinator's
1624
+ `saveToolState` hook ([Persistence Integration](#persistence-integration)):
1709
1625
 
1710
1626
  ```typescript
1711
1627
  {
1712
- "demo-assessment:section-1:question-1:mc1": {
1628
+ "demo-assessment:section-1:attempt-1:question-1:mc1": {
1713
1629
  "answerEliminator": {
1714
1630
  "eliminatedChoices": ["choice-b", "choice-d"]
1715
1631
  },
@@ -1726,9 +1642,9 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1726
1642
  - Tool preferences
1727
1643
  - UI state
1728
1644
 
1729
- ### PIE Session Data (Persistent)
1645
+ ### PIE session data
1730
1646
 
1731
- **Sent to server** for scoring:
1647
+ The host persists it, and its backend scores it:
1732
1648
 
1733
1649
  ```typescript
1734
1650
  {
@@ -1748,11 +1664,7 @@ The toolkit enforces a clear separation between ephemeral tool state and persist
1748
1664
 
1749
1665
  ## Examples
1750
1666
 
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
1667
+ See [section-demos](../../apps/section-demos/) for complete examples, among them "Demo 3: Three Questions, One Passage", "Demo 4: TTS with SSML Extraction" and "Two Passages with One Question".
1756
1668
 
1757
1669
  ## TypeScript Support
1758
1670
 
@@ -1776,31 +1688,27 @@ from tool configuration. Two sanitization layers apply:
1776
1688
  `pie-item-player`. See
1777
1689
  [pie-item-player README](../item-player/README.md#content-trust-boundary)
1778
1690
  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>`.
1691
+ The player also wraps overwide images and tables in scrollable regions
1692
+ ([reflow wrappers](../item-player/README.md#reflow-wrappers)).
1791
1693
  - **Tool icons and SSML** - tool-registered icon markup is parsed and
1792
1694
  DOMPurified inside the toolbar at render time; SSML payloads are
1793
1695
  restricted to an allow-listed subset of SSML tags/attributes before
1794
1696
  being forwarded to TTS providers. Do not ship tools that rely on raw
1795
1697
  `<script>` or event-handler attributes in their icon strings.
1796
1698
 
1699
+ [Security](../../docs/security/readme.md) sets out the full trust boundary and
1700
+ the host's obligations.
1701
+
1797
1702
  ## Related Documentation
1798
1703
 
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
1704
+ - [Tool Registry Reference](docs/TOOL_REGISTRY.md) - Registry-based tool management and AfA PNP 3.0 profile support
1705
+ - [PNP Configuration Guide](docs/PNP_CONFIGURATION.md) - How to configure student profiles, district policies, and governance rules
1706
+ - [Configuring Tools](../../docs/tools-and-accomodations/tool_provider_system.md) - Placement, policy and provider configuration
1707
+ - [Toolkit Tool Host Contract](../../docs/tools-and-accomodations/tool_host_contract.md) - Runtime guarantees between hosts and toolkit-managed tools
1708
+ - [Tools and Accommodations Architecture](../../docs/tools-and-accomodations/architecture.md) - Tool design, scopes and activation
1709
+ - [Assessment toolkit in the architecture overview](../../docs/architecture/architecture.md#assessment-toolkit) - Where the toolkit sits among the players
1802
1710
  - [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
1711
+ - [Section Player Architecture](../section-player/ARCHITECTURE.md#layered-runtime-engine) - Layered runtime engine, kernel/toolkit wiring, lifecycle emit invariant
1804
1712
  - [Framework-Owned Error Handling](../../docs/tools-and-accomodations/framework-owned-error-handling.md) - Canonical framework error model/events and fallback behavior
1805
1713
  - [Safe Custom Tool Configuration](../../docs/tools-and-accomodations/safe-custom-tool-config.md) - Host-side config patterns and validation guidance
1806
1714
  - [Architecture Overview](../../docs/architecture/architecture.md) - Complete system architecture