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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -1,27 +1,20 @@
1
1
  /**
2
- * Tool Registry
2
+ * Tool registry: the registration contract every tool declares itself with,
3
+ * the shape validation `register` and `override` run, and the `ToolRegistry`
4
+ * class.
3
5
  *
4
- * Central registry for all assessment tools. Manages tool metadata, visibility logic,
5
- * and button/instance creation. Supports dynamic registration and override by integrators.
6
+ * Layout: the exported contract types come first, ending with
7
+ * `ToolRegistration`, then `assertToolRegistrationShape`, then the class. Its
8
+ * members run from mutation and change observation, through queries and the
9
+ * filters for the second and third passes of tool resolution (relevance in
10
+ * `filterVisibleInContext`, applicability in `filterDecidedToolIds`; the policy
11
+ * engine owns the first), to module loading with its undefined-element
12
+ * warnings and the toolbar and surface render helpers.
6
13
  */
7
14
  import { dynamicMessageKey } from "@pie-players/pie-players-shared/i18n/provider";
8
- import { normalizeToolAlias } from "./tools-config-normalizer.js";
9
- /**
10
- * The id a registration's provider registers under for one tool config: the
11
- * descriptor's answer, then the config's `provider.id`, then the tool id. `null`
12
- * when the registration carries no provider.
13
- *
14
- * The coordinator registers providers under this id and the section player looks
15
- * for them in a host-supplied coordinator by it, so the two resolve it one way.
16
- */
17
- export function resolveToolProviderId(registration, config) {
18
- const descriptor = registration.provider;
19
- if (!descriptor)
20
- return null;
21
- return (descriptor.getProviderId?.(config) ??
22
- config?.provider?.id ??
23
- registration.toolId);
24
- }
15
+ import { resolveToolTag, } from "../tools/tool-tag-map.js";
16
+ import { PENDING_INPUT_WARNING_DELAY_MS, warnOncePerDocument, } from "../runtime/page-warnings.js";
17
+ const logToolCallbackFailure = (toolId, phase, error) => console.error(`[ToolRegistry] Tool '${toolId}' ${phase} check threw:`, error);
25
18
  /**
26
19
  * A registration's display name in the interface locale.
27
20
  *
@@ -163,11 +156,6 @@ function assertToolRegistrationShape(registration) {
163
156
  registration.singletonScope !== "section") {
164
157
  throw new Error(`Invalid tool registration "${registration.toolId}": selection-gateway tools must declare singletonScope "section".`);
165
158
  }
166
- if (registration.pnpSupportIds !== undefined &&
167
- (!Array.isArray(registration.pnpSupportIds) ||
168
- registration.pnpSupportIds.some((pnpId) => typeof pnpId !== "string" || pnpId.trim().length === 0))) {
169
- throw new Error(`Invalid tool registration "${registration.toolId}": "pnpSupportIds" must be an array of non-empty strings.`);
170
- }
171
159
  if (registration.activation !== "region" &&
172
160
  typeof registration.isVisibleInContext !== "function") {
173
161
  throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function.`);
@@ -186,12 +174,6 @@ function assertToolRegistrationShape(registration) {
186
174
  typeof registration.requiresAuthoredContent.resolve !== "function") {
187
175
  throw new Error(`Invalid tool registration "${registration.toolId}": "requiresAuthoredContent" must be an object with a "resolve" function.`);
188
176
  }
189
- if (!registration.pnpSupportIds?.length) {
190
- // A content dependency's second job is keeping the capability out of a
191
- // wholesale grant, and a host filters that by support id. Declaring one
192
- // with no id to filter on would silently drop that guarantee.
193
- throw new Error(`Invalid tool registration "${registration.toolId}": "requiresAuthoredContent" requires at least one entry in "pnpSupportIds", which is what a host filters a default grant list on.`);
194
- }
195
177
  }
196
178
  if (registration.renderToolbar !== undefined) {
197
179
  if (typeof registration.renderToolbar !== "function") {
@@ -209,10 +191,12 @@ function assertToolRegistrationShape(registration) {
209
191
  */
210
192
  export class ToolRegistry {
211
193
  tools = new Map();
212
- pnpIndex = new Map(); // pnpSupportId → Set<toolId>
213
194
  componentOverrides = {};
195
+ watchedUndefinedToolElements = new Set();
214
196
  moduleLoaders = new Map();
215
197
  loadedToolModules = new Set();
198
+ warnedLoaderReplacements = new Set();
199
+ warnedUnregisteredAllowedTools = new Set();
216
200
  moduleLoadPromises = new Map();
217
201
  changeListeners = new Set();
218
202
  emitChange(event) {
@@ -240,22 +224,19 @@ export class ToolRegistry {
240
224
  };
241
225
  }
242
226
  /**
243
- * Normalize a single tool alias to canonical toolId.
227
+ * Normalize a single tool id (trims surrounding whitespace).
244
228
  */
245
229
  normalizeToolId(toolId) {
246
- return normalizeToolAlias(toolId);
230
+ return toolId.trim();
247
231
  }
248
232
  /**
249
- * Normalize a list of tool aliases to canonical toolIds.
233
+ * Normalize a list of tool ids (trims surrounding whitespace).
250
234
  */
251
235
  normalizeToolIds(toolIds) {
252
236
  return toolIds.map((toolId) => this.normalizeToolId(toolId));
253
237
  }
254
238
  /**
255
- * Register a tool
256
- *
257
- * @param registration - Tool registration
258
- * @throws Error if toolId is already registered
239
+ * Register a tool. Throws when its `toolId` is already registered.
259
240
  */
260
241
  register(registration) {
261
242
  assertToolRegistrationShape(registration);
@@ -263,114 +244,44 @@ export class ToolRegistry {
263
244
  throw new Error(`Tool '${registration.toolId}' is already registered`);
264
245
  }
265
246
  this.tools.set(registration.toolId, registration);
266
- // Index PNP support IDs
267
- if (registration.pnpSupportIds) {
268
- for (const pnpId of registration.pnpSupportIds) {
269
- if (!this.pnpIndex.has(pnpId)) {
270
- this.pnpIndex.set(pnpId, new Set());
271
- }
272
- this.pnpIndex.get(pnpId).add(registration.toolId);
273
- }
274
- }
275
247
  this.emitChange({ kind: "register", toolIds: [registration.toolId] });
276
248
  }
277
249
  /**
278
- * Override an existing tool registration
279
- *
280
- * @param registration - New tool registration (must have existing toolId)
250
+ * Replace an existing tool registration. Throws when its `toolId` is not
251
+ * registered.
281
252
  */
282
253
  override(registration) {
283
254
  assertToolRegistrationShape(registration);
284
255
  if (!this.tools.has(registration.toolId)) {
285
256
  throw new Error(`Cannot override non-existent tool '${registration.toolId}'`);
286
257
  }
287
- // Remove old PNP index entries
288
- const oldReg = this.tools.get(registration.toolId);
289
- if (oldReg.pnpSupportIds) {
290
- for (const pnpId of oldReg.pnpSupportIds) {
291
- this.pnpIndex.get(pnpId)?.delete(registration.toolId);
292
- }
293
- }
294
- // Add new registration
295
258
  this.tools.set(registration.toolId, registration);
296
- // Re-index PNP support IDs
297
- if (registration.pnpSupportIds) {
298
- for (const pnpId of registration.pnpSupportIds) {
299
- if (!this.pnpIndex.has(pnpId)) {
300
- this.pnpIndex.set(pnpId, new Set());
301
- }
302
- this.pnpIndex.get(pnpId).add(registration.toolId);
303
- }
304
- }
305
259
  this.emitChange({ kind: "override", toolIds: [registration.toolId] });
306
260
  }
307
261
  /**
308
- * Unregister a tool
309
- *
310
- * @param toolId - Tool ID to remove
262
+ * Unregister a tool. An unknown id is a no-op and emits no change.
311
263
  */
312
264
  unregister(toolId) {
313
- const reg = this.tools.get(toolId);
314
- if (!reg)
265
+ if (!this.tools.has(toolId))
315
266
  return;
316
- // Remove PNP index entries
317
- if (reg.pnpSupportIds) {
318
- for (const pnpId of reg.pnpSupportIds) {
319
- this.pnpIndex.get(pnpId)?.delete(toolId);
320
- }
321
- }
322
267
  this.tools.delete(toolId);
323
268
  this.emitChange({ kind: "unregister", toolIds: [toolId] });
324
269
  }
325
- /**
326
- * Get a tool registration by ID
327
- *
328
- * @param toolId - Tool ID
329
- * @returns Tool registration or undefined
330
- */
331
270
  get(toolId) {
332
271
  return this.tools.get(toolId);
333
272
  }
334
- /**
335
- * Check if a tool is registered
336
- *
337
- * @param toolId - Tool ID
338
- * @returns true if registered
339
- */
340
273
  has(toolId) {
341
274
  return this.tools.has(toolId);
342
275
  }
343
- /**
344
- * Get all registered tool IDs
345
- *
346
- * @returns Array of tool IDs
347
- */
276
+ /** Registered tool ids, in registration order. */
348
277
  getAllToolIds() {
349
278
  return Array.from(this.tools.keys());
350
279
  }
351
- /**
352
- * Get all tool registrations
353
- *
354
- * @returns Array of tool registrations
355
- */
280
+ /** Registrations, in registration order. */
356
281
  getAllTools() {
357
282
  return Array.from(this.tools.values());
358
283
  }
359
- /**
360
- * Find tool IDs that support a given PNP support ID
361
- *
362
- * @param pnpSupportId - PNP support ID (e.g., 'calculator')
363
- * @returns Set of tool IDs that support this PNP ID
364
- */
365
- getToolsByPNPSupport(pnpSupportId) {
366
- return this.pnpIndex.get(pnpSupportId) || new Set();
367
- }
368
- /**
369
- * Get tools that support a specific level
370
- *
371
- * @param level - Tool level (assessment, section, item, passage, element)
372
- * @returns Array of tool registrations that support this level
373
- */
284
+ /** Registrations whose `supportedLevels` include `level`. */
374
285
  getToolsByLevel(level) {
375
286
  return this.getAllTools().filter((tool) => tool.supportedLevels.includes(level));
376
287
  }
@@ -401,21 +312,18 @@ export class ToolRegistry {
401
312
  tool.surfaces?.includes(surface));
402
313
  }
403
314
  /**
404
- * Support ids belonging to capabilities that need authored content.
315
+ * Support ids, which are tool ids, of the capabilities that need authored
316
+ * content.
405
317
  *
406
- * What a host filters a default grant list on, in place of the compile-time
407
- * exclusion array this replaced: granting one of these wholesale grants an
408
- * accommodation to learners with no documented need for it.
318
+ * What a host filters a default grant list on: granting one of these
319
+ * wholesale grants an accommodation to learners with no documented need
320
+ * for it.
409
321
  */
410
322
  getContentDependentSupportIds() {
411
- const ids = new Set();
412
- for (const tool of this.getAllTools()) {
413
- if (!tool.requiresAuthoredContent)
414
- continue;
415
- for (const supportId of tool.pnpSupportIds || [])
416
- ids.add(supportId);
417
- }
418
- return [...ids].sort();
323
+ return this.getAllTools()
324
+ .filter((tool) => tool.requiresAuthoredContent)
325
+ .map((tool) => tool.toolId)
326
+ .sort();
419
327
  }
420
328
  /**
421
329
  * Filter tool IDs by activation type.
@@ -424,36 +332,37 @@ export class ToolRegistry {
424
332
  return toolIds.filter((toolId) => this.getToolActivation(toolId) === activation);
425
333
  }
426
334
  /**
427
- * Filter tools by visibility in a given context
335
+ * Pass 2 of tool resolution: the registrations among `allowedToolIds` (the
336
+ * ids policy allowed in pass 1) that support `context.level` and answer
337
+ * `isVisibleInContext` with `true`.
428
338
  *
429
- * Pass 2 of the two-pass model: Given a list of allowed tool IDs (from Pass 1),
430
- * ask each tool if it's relevant in this context.
431
- *
432
- * @param allowedToolIds - Tool IDs that passed Pass 1 (orchestrator approval)
433
- * @param context - Context to evaluate
434
- * @returns Array of visible tool registrations
339
+ * @param onFailure - Receives a relevance check's throw; the tool is then not
340
+ * visible. Logs when omitted.
435
341
  */
436
- filterVisibleInContext(allowedToolIds, context) {
342
+ filterVisibleInContext(allowedToolIds, context, onFailure = logToolCallbackFailure) {
437
343
  const visible = [];
438
344
  for (const toolId of allowedToolIds) {
439
345
  const tool = this.get(toolId);
440
346
  if (!tool) {
441
- console.warn(`Tool '${toolId}' is allowed but not registered`);
347
+ // Toolbars filter on every render; one warning per id carries it.
348
+ if (!this.warnedUnregisteredAllowedTools.has(toolId)) {
349
+ this.warnedUnregisteredAllowedTools.add(toolId);
350
+ console.warn(`Tool '${toolId}' is allowed but not registered`);
351
+ }
442
352
  continue;
443
353
  }
444
- // Check if tool supports this level
445
354
  if (!tool.supportedLevels.includes(context.level)) {
446
355
  continue;
447
356
  }
448
- // Pass 2: Ask tool if it's relevant. A region capability declares no
449
- // answer and has no toolbar presence, so it is never visible here.
357
+ // A region capability declares no answer and has no toolbar presence,
358
+ // so it is never visible here.
450
359
  try {
451
360
  if (tool.isVisibleInContext?.(context)) {
452
361
  visible.push(tool);
453
362
  }
454
363
  }
455
364
  catch (error) {
456
- console.error(`Error evaluating visibility for tool '${toolId}':`, error);
365
+ onFailure(toolId, "tool-visibility", error);
457
366
  }
458
367
  }
459
368
  return visible;
@@ -470,8 +379,10 @@ export class ToolRegistry {
470
379
  *
471
380
  * @param toolId - Tool to ask
472
381
  * @param contexts - Every context the tool could act on at this placement
382
+ * @param onFailure - Receives the gate's throw; the tool then counts as
383
+ * applicable. Logs when omitted.
473
384
  */
474
- isApplicableToAnyContext(toolId, contexts) {
385
+ isApplicableToAnyContext(toolId, contexts, onFailure = logToolCallbackFailure) {
475
386
  const tool = this.get(toolId);
476
387
  if (!tool?.isApplicableToContent)
477
388
  return true;
@@ -482,24 +393,70 @@ export class ToolRegistry {
482
393
  return tool.isApplicableToContent?.(context) ?? true;
483
394
  }
484
395
  catch (error) {
485
- console.error(`Error evaluating applicability for tool '${toolId}':`, error);
396
+ onFailure(toolId, "tool-applicability", error);
486
397
  // A gate that throws has not established that the tool is useless.
487
398
  return true;
488
399
  }
489
400
  });
490
401
  }
491
402
  /**
492
- * Get tool metadata for building UIs
493
- * Useful for building PNP configuration interfaces
403
+ * Passes 2 and 3 over a policy decision's tools: the ids a surface at `level`
404
+ * shows against `contexts`, in decision order. This is the rule a toolbar
405
+ * applies, for a host that builds its own tool surface.
494
406
  *
495
- * @returns Array of tool metadata (id, name, description, pnpSupportIds)
407
+ * - A tool that does not support `level` is dropped.
408
+ * - Relevance (`isVisibleInContext`) keeps a tool visible in any one context.
409
+ * It does not run for a granted entry (`required` or `alwaysAvailable`),
410
+ * at section level, whose relevance is item-dependent, or when `contexts`
411
+ * is empty, since content that has not resolved cannot rule a tool out.
412
+ * - Applicability (`isApplicableToContent`) then removes a tool that can act
413
+ * on none of `contexts`, granted or not. It does not run at section level.
414
+ *
415
+ * @param entries - The decision's surviving entries (Pass 1), such as
416
+ * `ToolPolicyDecision.visibleTools`
417
+ * @param level - The level of the surface the tools render on
418
+ * @param contexts - Every context the tools could act on at that surface
419
+ * @param onFailure - Receives a relevance or applicability check's throw.
420
+ * Logs when omitted.
421
+ */
422
+ filterDecidedToolIds(entries, level, contexts, onFailure = logToolCallbackFailure) {
423
+ const granted = new Set();
424
+ const decided = [];
425
+ for (const entry of entries) {
426
+ const toolId = this.normalizeToolId(entry.toolId);
427
+ if (!toolId)
428
+ continue;
429
+ if (entry.required || entry.alwaysAvailable)
430
+ granted.add(toolId);
431
+ if (decided.includes(toolId))
432
+ continue;
433
+ if (!this.get(toolId)?.supportedLevels.includes(level))
434
+ continue;
435
+ decided.push(toolId);
436
+ }
437
+ if (level === "section")
438
+ return decided;
439
+ const relevant = new Set(contexts.length === 0
440
+ ? decided
441
+ : decided.filter((toolId) => granted.has(toolId)));
442
+ for (const context of contexts) {
443
+ const ungranted = decided.filter((toolId) => !relevant.has(toolId));
444
+ for (const tool of this.filterVisibleInContext(ungranted, context, onFailure)) {
445
+ relevant.add(tool.toolId);
446
+ }
447
+ }
448
+ return decided.filter((toolId) => relevant.has(toolId) &&
449
+ this.isApplicableToAnyContext(toolId, contexts, onFailure));
450
+ }
451
+ /**
452
+ * Plain metadata for every registration, in registration order, for UIs
453
+ * such as a PNP configuration screen.
496
454
  */
497
455
  getToolMetadata() {
498
456
  return this.getAllTools().map((tool) => ({
499
457
  toolId: tool.toolId,
500
458
  name: tool.name,
501
459
  description: tool.description,
502
- pnpSupportIds: tool.pnpSupportIds || [],
503
460
  supportedLevels: tool.supportedLevels,
504
461
  activation: tool.activation || "toolbar-toggle",
505
462
  singletonScope: tool.singletonScope || null,
@@ -508,25 +465,6 @@ export class ToolRegistry {
508
465
  contentDependencyDescription: tool.requiresAuthoredContent?.description ?? null,
509
466
  }));
510
467
  }
511
- /**
512
- * Generate PNP support IDs from enabled tools
513
- * Useful for creating PNP profiles
514
- *
515
- * @param enabledToolIds - Tool IDs to enable
516
- * @returns Array of unique PNP support IDs
517
- */
518
- generatePNPSupportsFromTools(enabledToolIds) {
519
- const pnpSupports = new Set();
520
- for (const toolId of enabledToolIds) {
521
- const tool = this.get(toolId);
522
- if (tool?.pnpSupportIds) {
523
- for (const pnpId of tool.pnpSupportIds) {
524
- pnpSupports.add(pnpId);
525
- }
526
- }
527
- }
528
- return Array.from(pnpSupports);
529
- }
530
468
  /**
531
469
  * Clear all registrations (useful for testing)
532
470
  */
@@ -535,7 +473,6 @@ export class ToolRegistry {
535
473
  if (toolIds.length === 0)
536
474
  return;
537
475
  this.tools.clear();
538
- this.pnpIndex.clear();
539
476
  this.emitChange({ kind: "clear", toolIds });
540
477
  }
541
478
  /**
@@ -550,6 +487,8 @@ export class ToolRegistry {
550
487
  /**
551
488
  * Register lazy module loaders by toolId.
552
489
  * Toolbars call ensureToolModuleLoaded(toolId) before instance creation.
490
+ * A tool whose module has loaded, or is loading, keeps its loader: its
491
+ * elements are already defined, so a replacement could never take effect.
553
492
  */
554
493
  setToolModuleLoaders(loaders) {
555
494
  const entries = Object.entries(loaders).filter((entry) => entry[1] !== undefined);
@@ -563,6 +502,11 @@ export class ToolRegistry {
563
502
  for (const [toolId, loader] of entries) {
564
503
  if (this.moduleLoaders.get(toolId) === loader)
565
504
  continue;
505
+ if (this.loadedToolModules.has(toolId) ||
506
+ this.moduleLoadPromises.has(toolId)) {
507
+ this.warnLoaderReplacedAfterLoad(toolId);
508
+ continue;
509
+ }
566
510
  this.moduleLoaders.set(toolId, loader);
567
511
  changedToolIds.push(toolId);
568
512
  }
@@ -570,6 +514,12 @@ export class ToolRegistry {
570
514
  this.emitChange({ kind: "module-loaders", toolIds: changedToolIds });
571
515
  }
572
516
  }
517
+ warnLoaderReplacedAfterLoad(toolId) {
518
+ if (this.warnedLoaderReplacements.has(toolId))
519
+ return;
520
+ this.warnedLoaderReplacements.add(toolId);
521
+ console.warn(`[ToolRegistry] Ignored a new module loader for "${toolId}": its module already loaded, so the elements it defines stay in place. Register loaders before the tool first renders.`);
522
+ }
573
523
  /**
574
524
  * Ensure tool module side-effects are loaded exactly once.
575
525
  * Safe to call repeatedly; concurrent callers share the same promise.
@@ -583,11 +533,14 @@ export class ToolRegistry {
583
533
  return;
584
534
  }
585
535
  const loader = this.moduleLoaders.get(toolId);
586
- if (!loader)
536
+ if (!loader) {
537
+ this.watchUndefinedToolElement(toolId);
587
538
  return;
539
+ }
588
540
  const loadPromise = (async () => {
589
541
  await loader();
590
542
  this.loadedToolModules.add(toolId);
543
+ this.warnIfLoadedToolElementUndefined(toolId);
591
544
  })();
592
545
  this.moduleLoadPromises.set(toolId, loadPromise);
593
546
  try {
@@ -598,10 +551,70 @@ export class ToolRegistry {
598
551
  }
599
552
  }
600
553
  /**
601
- * Ensure a set of tool modules are loaded.
554
+ * The undefined element tag a tool would render, or `null` when the tool is
555
+ * built by a component factory, its element is defined, or there is no DOM.
556
+ */
557
+ undefinedToolElementTag(toolId) {
558
+ if (typeof customElements === "undefined" || typeof document === "undefined") {
559
+ return null;
560
+ }
561
+ const overrides = this.componentOverrides;
562
+ if (overrides.toolComponentFactories?.[toolId])
563
+ return null;
564
+ let tagName;
565
+ try {
566
+ tagName = resolveToolTag(toolId, overrides);
567
+ }
568
+ catch {
569
+ // Element creation reports the missing tag mapping.
570
+ return null;
571
+ }
572
+ return customElements.get(tagName) ? null : tagName;
573
+ }
574
+ /**
575
+ * Warn when a tool's module loader finished without defining the element the
576
+ * tag map points the tool at, as when a host remaps the tag but keeps a
577
+ * loader that defines the packaged element. The tool otherwise renders blank.
578
+ */
579
+ warnIfLoadedToolElementUndefined(toolId) {
580
+ const tagName = this.undefinedToolElementTag(toolId);
581
+ if (!tagName)
582
+ return;
583
+ warnOncePerDocument(document, `loadedToolElementUndefined.${toolId}.${tagName}`, `[ToolRegistry] Tool "${toolId}" renders <${tagName}>, but its module loader finished without defining it, so the tool renders blank. The tag map and the loader disagree: map "${toolId}" to the tag its loader defines, or register a loader that defines <${tagName}>. Reported once per page.`);
584
+ }
585
+ /**
586
+ * Warn when a tool with no module loader still has no element definition
587
+ * after the pending-input delay. A registry built without loaders otherwise
588
+ * renders the tool as an unknown element with no error.
589
+ */
590
+ watchUndefinedToolElement(toolId) {
591
+ if (this.watchedUndefinedToolElements.has(toolId))
592
+ return;
593
+ const tagName = this.undefinedToolElementTag(toolId);
594
+ if (!tagName)
595
+ return;
596
+ this.watchedUndefinedToolElements.add(toolId);
597
+ const doc = document;
598
+ const timer = setTimeout(() => {
599
+ if (customElements.get(tagName))
600
+ return;
601
+ warnOncePerDocument(doc, `undefinedToolElement.${toolId}`, `[ToolRegistry] Tool "${toolId}" renders <${tagName}>, which is still undefined after ${PENDING_INPUT_WARNING_DELAY_MS / 1000} s, and its registry has no module loader for it. Register one with setToolModuleLoaders or import the tool's package before it renders; a registry from createPackagedToolRegistry has the packaged loaders unless its toolModuleLoaders replaced them. Reported once per page.`);
602
+ }, PENDING_INPUT_WARNING_DELAY_MS);
603
+ void customElements.whenDefined(tagName).then(() => clearTimeout(timer));
604
+ }
605
+ /**
606
+ * Load a set of tool modules, each to completion, and resolve with the error
607
+ * of every tool whose module failed, by tool id. One failure leaves the other
608
+ * tools loaded and usable. A failed tool is retried on the next call.
602
609
  */
603
610
  async ensureToolModulesLoaded(toolIds) {
604
- await Promise.all(toolIds.map((toolId) => this.ensureToolModuleLoaded(toolId)));
611
+ const results = await Promise.allSettled(toolIds.map((toolId) => this.ensureToolModuleLoaded(toolId)));
612
+ const failures = new Map();
613
+ results.forEach((result, index) => {
614
+ if (result.status === "rejected")
615
+ failures.set(toolIds[index], result.reason);
616
+ });
617
+ return failures;
605
618
  }
606
619
  /**
607
620
  * Whether a tool module has already been loaded.
@@ -625,10 +638,7 @@ export class ToolRegistry {
625
638
  }
626
639
  const mergedContext = {
627
640
  ...toolbarContext,
628
- componentOverrides: {
629
- ...(this.componentOverrides || {}),
630
- ...(toolbarContext.componentOverrides || {}),
631
- },
641
+ componentOverrides: this.mergeComponentOverrides(toolbarContext.componentOverrides),
632
642
  };
633
643
  return tool.renderToolbar(context, mergedContext);
634
644
  }
@@ -651,10 +661,14 @@ export class ToolRegistry {
651
661
  }
652
662
  return tool.renderSurface({
653
663
  ...context,
654
- componentOverrides: {
655
- ...(this.componentOverrides || {}),
656
- ...(context.componentOverrides || {}),
657
- },
664
+ componentOverrides: this.mergeComponentOverrides(context.componentOverrides),
658
665
  });
659
666
  }
667
+ /** The registry's component overrides, with the caller's taking precedence. */
668
+ mergeComponentOverrides(contextOverrides) {
669
+ return {
670
+ ...(this.componentOverrides || {}),
671
+ ...(contextOverrides || {}),
672
+ };
673
+ }
660
674
  }