@pie-players/pie-assessment-toolkit 0.3.72 → 0.3.74

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 (108) hide show
  1. package/README.md +171 -56
  2. package/dist/attempt/AssessmentSession.d.ts +1 -5
  3. package/dist/attempt/AssessmentSession.js +0 -29
  4. package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
  5. package/dist/components/ItemToolBar.custom-element.js +1 -1
  6. package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  8. package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +40 -0
  11. package/dist/components/chunks/ItemToolBar-gmrryv2v.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +4 -0
  13. package/dist/components/chunks/ItemToolBar-wr2myaah.js +2 -0
  14. package/dist/components/chunks/sre-engine-wf9s43gg.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +5 -0
  16. package/dist/context/runtime-context-consumer.js +5 -38
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +8 -9
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +8 -2
  20. package/dist/policy/core/ToolPolicyEngine.js +12 -0
  21. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +3 -3
  22. package/dist/runtime/adapter/SectionEngineAdapter.js +3 -3
  23. package/dist/runtime/registration-events.d.ts +3 -0
  24. package/dist/runtime/section-runtime-engine-host-context.d.ts +3 -9
  25. package/dist/runtime/section-runtime-engine-host-context.js +5 -43
  26. package/dist/runtime/tool-host-contract.d.ts +20 -0
  27. package/dist/runtime/tool-host-contract.js +21 -0
  28. package/dist/services/HighlightCoordinator.d.ts +1 -7
  29. package/dist/services/HighlightCoordinator.js +1 -3
  30. package/dist/services/TTSService.d.ts +15 -1
  31. package/dist/services/TTSService.js +88 -35
  32. package/dist/services/ToolCoordinator.d.ts +1 -7
  33. package/dist/services/ToolCoordinator.js +1 -3
  34. package/dist/services/ToolRegistry.d.ts +13 -6
  35. package/dist/services/ToolRegistry.js +19 -7
  36. package/dist/services/ToolkitCoordinator.d.ts +110 -18
  37. package/dist/services/ToolkitCoordinator.js +273 -85
  38. package/dist/services/catalog-owner.d.ts +11 -0
  39. package/dist/services/catalog-owner.js +25 -0
  40. package/dist/services/create-tools-config.d.ts +2 -2
  41. package/dist/services/framework-error.d.ts +8 -1
  42. package/dist/services/interfaces.d.ts +9 -9
  43. package/dist/services/pnp-standard-features.d.ts +1 -1
  44. package/dist/services/section-controller-types.d.ts +30 -0
  45. package/dist/services/spoken-audio-cards.js +1 -1
  46. package/dist/services/tool-config-validation.d.ts +13 -2
  47. package/dist/services/tool-config-validation.js +33 -16
  48. package/dist/services/tool-context.js +29 -13
  49. package/dist/services/tool-providers/TTSToolProvider.d.ts +27 -65
  50. package/dist/services/tool-providers/TTSToolProvider.js +47 -9
  51. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +4 -4
  52. package/dist/services/tool-providers/ToolProviderRegistry.js +4 -16
  53. package/dist/services/tool-providers/index.d.ts +0 -9
  54. package/dist/services/tool-providers/index.js +2 -4
  55. package/dist/services/tools-config-normalizer.d.ts +20 -1
  56. package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
  57. package/dist/services/tts/catalog-span-alignment.js +17 -142
  58. package/dist/services/tts/generated-speech/types.d.ts +0 -12
  59. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
  60. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +26 -1
  61. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
  62. package/dist/services/tts/math-alignment/index.d.ts +0 -5
  63. package/dist/services/tts/math-alignment/index.js +5 -25
  64. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
  65. package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
  66. package/dist/services/tts/math-alignment/types.d.ts +4 -2
  67. package/dist/services/tts/math-aware-text-processing.js +44 -2
  68. package/dist/services/tts/math-speech.d.ts +5 -1
  69. package/dist/services/tts/math-speech.js +9 -6
  70. package/dist/services/tts/sre-engine.d.ts +7 -0
  71. package/dist/services/tts/sre-engine.js +11 -0
  72. package/dist/services/tts/sre-locales.d.ts +14 -0
  73. package/dist/services/tts/sre-locales.js +57 -0
  74. package/dist/services/tts/text-processing.d.ts +0 -1
  75. package/dist/services/tts/text-processing.js +0 -1
  76. package/dist/services/tts-runtime-config.d.ts +21 -3
  77. package/dist/services/tts-runtime-config.js +11 -3
  78. package/dist/tools/client.d.ts +1 -3
  79. package/dist/tools/client.js +1 -5
  80. package/dist/tools/internal.d.ts +6 -5
  81. package/dist/tools/internal.js +6 -8
  82. package/package.json +17 -36
  83. package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
  84. package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
  85. package/dist/services/ToolConfigResolver.d.ts +0 -161
  86. package/dist/services/ToolConfigResolver.js +0 -229
  87. package/dist/services/catalog-media.d.ts +0 -56
  88. package/dist/services/catalog-media.js +0 -190
  89. package/dist/services/createDefaultToolRegistry.d.ts +0 -53
  90. package/dist/services/createDefaultToolRegistry.js +0 -45
  91. package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
  92. package/dist/services/tool-providers/CortexToolProvider.js +0 -32
  93. package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
  94. package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
  95. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
  96. package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
  97. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
  98. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
  99. package/dist/services/tts/visual-line-ranges.d.ts +0 -18
  100. package/dist/services/tts/visual-line-ranges.js +0 -226
  101. package/dist/tools/index.d.ts +0 -8
  102. package/dist/tools/index.js +0 -10
  103. package/dist/tools/response-discovery.d.ts +0 -61
  104. package/dist/tools/response-discovery.js +0 -113
  105. package/dist/tools/types.d.ts +0 -337
  106. package/dist/tools/types.js +0 -12
  107. package/dist/tools/variant-resolver.d.ts +0 -41
  108. package/dist/tools/variant-resolver.js +0 -207
@@ -14,7 +14,7 @@
14
14
  * Part of PIE Assessment Toolkit.
15
15
  */
16
16
  import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
17
- import { normalizeAndValidateToolsConfig, normalizeToolConfigStrictness, } from "./tool-config-validation.js";
17
+ import { collectToolConfigDiagnostics, normalizeToolConfigStrictness, reportToolConfigDiagnostics, } from "./tool-config-validation.js";
18
18
  import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
19
19
  import { ElementToolStateStore } from "./ElementToolStateStore.js";
20
20
  import { frameworkErrorFromCoordinatorContext, } from "./framework-error.js";
@@ -25,7 +25,7 @@ import { TTSService } from "./TTSService.js";
25
25
  import { BrowserTTSProvider } from "./tts/browser-provider.js";
26
26
  import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
27
27
  import { ToolProviderRegistry } from "./tool-providers/index.js";
28
- import { ToolRegistry } from "./ToolRegistry.js";
28
+ import { resolveToolProviderId, ToolRegistry } from "./ToolRegistry.js";
29
29
  import { ToolRequestRegistry } from "./tool-request.js";
30
30
  import { ToolPolicyEngine, } from "../policy/engine.js";
31
31
  import { resolveDefaultPnpEnforcement } from "../policy/internal.js";
@@ -81,9 +81,13 @@ const SECTION_SCOPED_EVENT_TYPES = [
81
81
  *
82
82
  * @example
83
83
  * ```typescript
84
+ * // createPackagedToolRegistry is exported by @pie-players/pie-default-tool-loaders
85
+ * const toolRegistry = createPackagedToolRegistry();
86
+ *
84
87
  * // Create coordinator with configuration
85
88
  * const coordinator = new ToolkitCoordinator({
86
89
  * assessmentId: 'demo-three-questions',
90
+ * toolRegistry,
87
91
  * tools: {
88
92
  * providers: {
89
93
  * textToSpeech: { enabled: true, backend: 'browser' },
@@ -96,7 +100,7 @@ const SECTION_SCOPED_EVENT_TYPES = [
96
100
  * });
97
101
  *
98
102
  * // Pass to section player
99
- * player.toolkitCoordinator = coordinator;
103
+ * player.runtime = { ...(player.runtime ?? {}), coordinator };
100
104
  *
101
105
  * // Access services directly
102
106
  * const ttsService = coordinator.ttsService;
@@ -127,6 +131,14 @@ export class ToolkitCoordinator {
127
131
  coordinatorReadyNotified = false;
128
132
  providerInitPromises = new Map();
129
133
  toolRegistry;
134
+ /** Whether the host passed `toolRegistry`, which then always stands. */
135
+ toolRegistrySupplied;
136
+ /**
137
+ * Whether the registry is final: supplied at construction, or adopted from
138
+ * the toolkit this coordinator is bound to. Until then the empty placeholder
139
+ * reports no missing registry, since the toolkit will supply one.
140
+ */
141
+ toolRegistrySettled;
130
142
  toolContextResolvers = new Map();
131
143
  toolContextResolverChangeListeners = new Set();
132
144
  toolRequests = new ToolRequestRegistry();
@@ -213,22 +225,40 @@ export class ToolkitCoordinator {
213
225
  * lifetime for no new information.
214
226
  */
215
227
  reportedUnboundFeaturePolicy = false;
216
- static resolveConfig(config) {
228
+ /**
229
+ * Whether {@link _initializeTTS} has already reported a non-browser backend
230
+ * falling back to browser speech because no `tts` provider is registered.
231
+ * Once per coordinator: a text-to-speech config change re-runs
232
+ * initialization, and the missing provider is the same gap each time.
233
+ */
234
+ reportedMissingTTSProvider = false;
235
+ /**
236
+ * The backend of a fallback {@link reportMissingTTSProvider} held back while
237
+ * the registry was unsettled, reported if the bound toolkit has no registry.
238
+ */
239
+ heldMissingTTSProviderBackend = null;
240
+ /**
241
+ * Whether {@link validateToolsConfig} has already reported validating with
242
+ * no registry (`tools.registryUnavailable`). Once per coordinator: every
243
+ * {@link updateToolConfig} and {@link updateToolsPlacement} re-validates, and
244
+ * the missing registry is the same gap each time.
245
+ */
246
+ reportedRegistryUnavailable = false;
247
+ resolveConfig(config) {
217
248
  const strictness = normalizeToolConfigStrictness(config.toolConfigStrictness);
218
- // An empty registry when the host supplies none. This package no longer
219
- // holds the packaged capability set, so there is nothing to fall back to —
220
- // a host that wants stock tools passes a registry from
221
- // `@pie-players/pie-default-tool-loaders`. Tool-id validation is skipped
222
- // against an empty registry (with a diagnostic saying so) rather than
249
+ // An empty placeholder when the host supplies none, replaced by the bound
250
+ // toolkit's registry in `adoptToolRegistry`. This package does not hold the
251
+ // packaged capability set, and importing it would be a dependency cycle.
252
+ // Tool-id validation is skipped against an empty registry rather than
223
253
  // rejecting every configured id.
224
254
  const toolRegistry = config.toolRegistry ?? new ToolRegistry();
225
255
  const normalized = config.deferToolConfigValidation === true
226
256
  ? normalizeToolsConfig(config.tools)
227
- : normalizeAndValidateToolsConfig(config.tools, {
257
+ : this.validateToolsConfig(config.tools, {
228
258
  strictness,
229
259
  source: "ToolkitCoordinator.init",
230
260
  toolRegistry,
231
- }).config;
261
+ });
232
262
  const defaultProviders = {
233
263
  textToSpeech: {
234
264
  enabled: true,
@@ -256,11 +286,29 @@ export class ToolkitCoordinator {
256
286
  },
257
287
  };
258
288
  }
289
+ /**
290
+ * `normalizeAndValidateToolsConfig`, except that `tools.registryUnavailable`
291
+ * is reported once per coordinator, whichever call validates.
292
+ */
293
+ validateToolsConfig(tools, options) {
294
+ const { config, diagnostics } = collectToolConfigDiagnostics(tools, options.toolRegistry);
295
+ const unreported = this.reportedRegistryUnavailable || !this.toolRegistrySettled
296
+ ? diagnostics.filter((entry) => entry.code !== "tools.registryUnavailable")
297
+ : diagnostics;
298
+ reportToolConfigDiagnostics(unreported, options);
299
+ if (options.strictness !== "off" &&
300
+ unreported.some((entry) => entry.code === "tools.registryUnavailable")) {
301
+ this.reportedRegistryUnavailable = true;
302
+ }
303
+ return config;
304
+ }
259
305
  constructor(config) {
260
306
  if (!config.assessmentId) {
261
307
  throw new Error("ToolkitCoordinator requires assessmentId in config");
262
308
  }
263
- const resolvedConfig = ToolkitCoordinator.resolveConfig(config);
309
+ this.toolRegistrySupplied = config.toolRegistry != null;
310
+ this.toolRegistrySettled = this.toolRegistrySupplied;
311
+ const resolvedConfig = this.resolveConfig(config);
264
312
  this.assessmentId = resolvedConfig.assessmentId;
265
313
  this.config = resolvedConfig;
266
314
  this.toolRegistry = resolvedConfig.toolRegistry ?? new ToolRegistry();
@@ -286,6 +334,9 @@ export class ToolkitCoordinator {
286
334
  this._registerToolProviders();
287
335
  // Initialize TTS service based on config
288
336
  this.ttsService = new TTSService();
337
+ // Selection read-aloud speaks through this service without the inline TTS
338
+ // tool ever having run, so it cannot rely on that tool to attach highlights.
339
+ this.ttsService.setHighlightCoordinator(this.highlightCoordinator);
289
340
  this.setupStatePersistenceHooks();
290
341
  // M8 PR 2 — construct the unified ToolPolicyEngine seeded with
291
342
  // the validated tools config. PNP/profile inputs (`assessment`,
@@ -527,6 +578,72 @@ export class ToolkitCoordinator {
527
578
  });
528
579
  return this.stateLoadPromise;
529
580
  }
581
+ reportMissingTTSProvider(backend) {
582
+ // The toolkit this coordinator binds to may still supply the provider.
583
+ if (!this.toolRegistrySettled) {
584
+ this.heldMissingTTSProviderBackend = backend;
585
+ return;
586
+ }
587
+ if (this.reportedMissingTTSProvider)
588
+ return;
589
+ this.reportedMissingTTSProvider = true;
590
+ console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "tts" tool provider is registered. A coordinator registers tool providers only from its \`toolRegistry\`, or from its toolkit's when constructed without one, so supply one that carries the text-to-speech registration — for the packaged capability set, \`createPackagedToolRegistry()\` from "@pie-players/pie-default-tool-loaders". Reported once per coordinator.`);
591
+ }
592
+ /**
593
+ * Take the registry of the toolkit this coordinator is bound to, when the host
594
+ * constructed it without one: the config is validated against it, its
595
+ * providers register, and text-to-speech re-initializes through it if it
596
+ * already started. The toolkit elements call this on binding. A registry
597
+ * passed at construction stands, and the first call wins; `null` records a
598
+ * toolkit without one, which reports the registry as missing.
599
+ *
600
+ * @returns Whether `registry` was adopted.
601
+ */
602
+ adoptToolRegistry(registry) {
603
+ if (this.disposePromise !== null || this.toolRegistrySettled)
604
+ return false;
605
+ this.toolRegistrySettled = true;
606
+ const heldBackend = this.heldMissingTTSProviderBackend;
607
+ this.heldMissingTTSProviderBackend = null;
608
+ const strictness = this.config.toolConfigStrictness ?? "error";
609
+ const source = "ToolkitCoordinator.adoptToolRegistry";
610
+ if (!registry) {
611
+ this.validateToolsConfig(this.config.tools, {
612
+ strictness,
613
+ source,
614
+ toolRegistry: this.toolRegistry,
615
+ });
616
+ if (heldBackend)
617
+ this.reportMissingTTSProvider(heldBackend);
618
+ return false;
619
+ }
620
+ this.toolRegistry = registry;
621
+ this.config.toolRegistry = registry;
622
+ try {
623
+ // Warn at most: the config was accepted at construction, and a binding
624
+ // toolkit has no caller to throw to.
625
+ this.config.tools = this.validateToolsConfig(this.config.tools, {
626
+ strictness: strictness === "off" ? "off" : "warn",
627
+ source,
628
+ toolRegistry: registry,
629
+ });
630
+ }
631
+ catch (err) {
632
+ console.warn("[ToolkitCoordinator] Tool config does not validate against the adopted tool registry:", err);
633
+ }
634
+ this.policyEngine.replaceToolRegistry(registry, this.config.tools);
635
+ const ttsStarted = this.ttsInitialized ||
636
+ this.ttsInitPromise !== undefined ||
637
+ this.ttsReconfigurePromise !== undefined;
638
+ for (const tool of this.getProviderDescriptorTools()) {
639
+ if (ttsStarted && tool.toolId === "textToSpeech")
640
+ continue;
641
+ void this.registerProviderFromTool(tool);
642
+ }
643
+ if (ttsStarted)
644
+ this.scheduleTTSReconfigure();
645
+ return true;
646
+ }
530
647
  /**
531
648
  * Register tool providers in the registry
532
649
  */
@@ -539,40 +656,52 @@ export class ToolkitCoordinator {
539
656
  getProviderDescriptorTools() {
540
657
  return this.toolRegistry.getAllTools().filter((tool) => !!tool.provider);
541
658
  }
659
+ /**
660
+ * Register the provider `tool`'s descriptor creates for the tool's current
661
+ * config, unless the tool is disabled or that provider id is registered. A
662
+ * descriptor that throws is reported the way a failed registration is.
663
+ */
542
664
  async registerProviderFromTool(tool) {
543
665
  const descriptor = tool.provider;
544
666
  if (!descriptor)
545
667
  return;
546
- const toolConfig = this.getToolConfig(tool.toolId) || undefined;
547
- if (toolConfig?.enabled === false)
548
- return;
549
- const providerId = descriptor.getProviderId?.(toolConfig) ??
550
- toolConfig?.provider?.id ??
551
- tool.toolId;
552
- if (this.toolProviderRegistry.has(providerId))
668
+ let providerId = null;
669
+ let registration;
670
+ try {
671
+ const toolConfig = this.getToolConfig(tool.toolId) || undefined;
672
+ if (toolConfig?.enabled === false)
673
+ return;
674
+ providerId = resolveToolProviderId(tool, toolConfig);
675
+ if (!providerId || this.toolProviderRegistry.has(providerId))
676
+ return;
677
+ const provider = descriptor.createProvider(toolConfig);
678
+ const initConfig = descriptor.getInitConfig?.(toolConfig) ??
679
+ toolConfig?.provider?.init ??
680
+ {};
681
+ const initConfigWithTelemetry = this.addToolTelemetryReporter({
682
+ toolId: tool.toolId,
683
+ providerId,
684
+ initConfig,
685
+ });
686
+ const registryTelemetry = this.createToolTelemetryForwarder({
687
+ toolId: tool.toolId,
688
+ providerId,
689
+ });
690
+ const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
691
+ toolConfig?.provider?.runtime?.authFetcher;
692
+ registration = {
693
+ provider,
694
+ config: initConfigWithTelemetry,
695
+ lazy: descriptor.lazy ?? true,
696
+ authFetcher,
697
+ onTelemetry: registryTelemetry,
698
+ };
699
+ }
700
+ catch (err) {
701
+ this.reportProviderRegisterFailure(err, providerId, tool.toolId);
553
702
  return;
554
- const provider = descriptor.createProvider(toolConfig);
555
- const initConfig = descriptor.getInitConfig?.(toolConfig) ??
556
- toolConfig?.provider?.init ??
557
- {};
558
- const initConfigWithTelemetry = this.addToolTelemetryReporter({
559
- toolId: tool.toolId,
560
- providerId,
561
- initConfig,
562
- });
563
- const registryTelemetry = this.createToolTelemetryForwarder({
564
- toolId: tool.toolId,
565
- providerId,
566
- });
567
- const authFetcher = descriptor.getAuthFetcher?.(toolConfig) ??
568
- toolConfig?.provider?.runtime?.authFetcher;
569
- await this.registerProvider(providerId, {
570
- provider,
571
- config: initConfigWithTelemetry,
572
- lazy: descriptor.lazy ?? true,
573
- authFetcher,
574
- onTelemetry: registryTelemetry,
575
- });
703
+ }
704
+ await this.registerProvider(providerId, registration);
576
705
  }
577
706
  createToolTelemetryForwarder(args) {
578
707
  return async (eventName, payload) => {
@@ -616,10 +745,22 @@ export class ToolkitCoordinator {
616
745
  });
617
746
  }
618
747
  catch (err) {
619
- console.warn(`[ToolkitCoordinator] Failed to register provider "${providerId}":`, err);
620
- this.handleError(err, { phase: "provider-register", providerId });
748
+ this.reportProviderRegisterFailure(err, providerId);
621
749
  }
622
750
  }
751
+ /**
752
+ * A console warning and a `provider-register` framework error. `providerId`
753
+ * is null when resolving it is what failed; the warning names the tool then.
754
+ */
755
+ reportProviderRegisterFailure(err, providerId, toolId) {
756
+ console.warn(providerId
757
+ ? `[ToolkitCoordinator] Failed to register provider "${providerId}":`
758
+ : `[ToolkitCoordinator] Failed to register the provider of tool "${toolId}":`, err);
759
+ this.handleError(err, {
760
+ phase: "provider-register",
761
+ providerId: providerId ?? undefined,
762
+ });
763
+ }
623
764
  async ensureProviderReady(providerId) {
624
765
  this.assertNotDisposed();
625
766
  const existing = this.providerInitPromises.get(providerId);
@@ -1505,6 +1646,9 @@ export class ToolkitCoordinator {
1505
1646
  console.warn("[ToolkitCoordinator] Failed to initialize TTS via registry, falling back to browser provider:", normalized);
1506
1647
  }
1507
1648
  }
1649
+ else if (resolvedBackend !== "browser") {
1650
+ this.reportMissingTTSProvider(resolvedBackend);
1651
+ }
1508
1652
  // Fallback to browser provider
1509
1653
  const provider = new BrowserTTSProvider();
1510
1654
  try {
@@ -1688,8 +1832,7 @@ export class ToolkitCoordinator {
1688
1832
  };
1689
1833
  }
1690
1834
  getTTSConfigFromProviders() {
1691
- const providers = this.config.tools?.providers || {};
1692
- return providers.textToSpeech;
1835
+ return this.config.tools?.providers?.textToSpeech;
1693
1836
  }
1694
1837
  assertCanonicalToolId(toolId) {
1695
1838
  if (typeof toolId !== "string" || toolId.trim().length === 0) {
@@ -1703,8 +1846,8 @@ export class ToolkitCoordinator {
1703
1846
  // host's every tool-config call into an exception — including the calls
1704
1847
  // this coordinator's own default-provider block provokes, which is how a
1705
1848
  // host that passes no registry ended up unable to read its own config.
1706
- // `normalizeAndValidateToolsConfig` already reports the missing registry
1707
- // once, as `tools.registryUnavailable`; a second report per call is noise.
1849
+ // Validation reports the missing registry once per coordinator, as
1850
+ // `tools.registryUnavailable`; a report per call would be noise.
1708
1851
  if (this.toolRegistry.getAllToolIds().length === 0)
1709
1852
  return;
1710
1853
  if (!this.toolRegistry.get(toolId)) {
@@ -1752,26 +1895,20 @@ export class ToolkitCoordinator {
1752
1895
  // Enabled by default unless explicitly set to false
1753
1896
  return toolConfig?.enabled !== false;
1754
1897
  }
1755
- /**
1756
- * Get tool configuration.
1757
- *
1758
- * @param toolId Tool identifier
1759
- * @returns Tool configuration or null if not configured
1760
- */
1761
1898
  getToolConfig(toolId) {
1762
1899
  this.assertCanonicalToolId(toolId);
1763
- return (this.config.tools?.providers?.[toolId] || null);
1900
+ return this.config.tools?.providers?.[toolId] || null;
1764
1901
  }
1765
1902
  updateToolConfig(toolId, updates) {
1766
1903
  // Update config
1767
1904
  this.assertCanonicalToolId(toolId);
1768
1905
  const current = this.getToolConfig(toolId) || {};
1769
1906
  if (!this.config.tools) {
1770
- this.config.tools = normalizeAndValidateToolsConfig(undefined, {
1907
+ this.config.tools = this.validateToolsConfig(undefined, {
1771
1908
  strictness: this.config.toolConfigStrictness ?? "error",
1772
1909
  source: "ToolkitCoordinator.updateToolConfig",
1773
1910
  toolRegistry: this.toolRegistry,
1774
- }).config;
1911
+ });
1775
1912
  }
1776
1913
  if (!this.config.tools.providers) {
1777
1914
  this.config.tools.providers = {};
@@ -1780,7 +1917,7 @@ export class ToolkitCoordinator {
1780
1917
  ...(this.config.tools.providers || {}),
1781
1918
  [toolId]: mergeToolConfigUpdate(toolId, current, updates),
1782
1919
  };
1783
- const validated = normalizeAndValidateToolsConfig({
1920
+ this.config.tools = this.validateToolsConfig({
1784
1921
  ...this.config.tools,
1785
1922
  providers: nextProviders,
1786
1923
  }, {
@@ -1788,7 +1925,6 @@ export class ToolkitCoordinator {
1788
1925
  source: "ToolkitCoordinator.updateToolConfig",
1789
1926
  toolRegistry: this.toolRegistry,
1790
1927
  });
1791
- this.config.tools = validated.config;
1792
1928
  // M8 PR 2 — keep the policy engine's tools input in lockstep
1793
1929
  // with the validated coordinator config. The engine emits an
1794
1930
  // `inputs` change event so subscribers (e.g. PR 3 toolbars) can
@@ -1798,7 +1934,7 @@ export class ToolkitCoordinator {
1798
1934
  });
1799
1935
  void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
1800
1936
  // Apply configuration changes to services
1801
- this._applyToolConfigChange(toolId, updates);
1937
+ this._applyToolConfigChange(toolId, current);
1802
1938
  }
1803
1939
  /**
1804
1940
  * Update the enabled tool list for one placement level.
@@ -1816,11 +1952,11 @@ export class ToolkitCoordinator {
1816
1952
  */
1817
1953
  updateToolsPlacement(partial) {
1818
1954
  if (!this.config.tools) {
1819
- this.config.tools = normalizeAndValidateToolsConfig(undefined, {
1955
+ this.config.tools = this.validateToolsConfig(undefined, {
1820
1956
  strictness: this.config.toolConfigStrictness ?? "error",
1821
1957
  source: "ToolkitCoordinator.updateToolsPlacement",
1822
1958
  toolRegistry: this.toolRegistry,
1823
- }).config;
1959
+ });
1824
1960
  }
1825
1961
  const currentPlacement = this.config.tools?.placement ?? {
1826
1962
  section: [],
@@ -1832,7 +1968,7 @@ export class ToolkitCoordinator {
1832
1968
  item: [...(partial.item ?? currentPlacement.item ?? [])],
1833
1969
  passage: [...(partial.passage ?? currentPlacement.passage ?? [])],
1834
1970
  };
1835
- const validated = normalizeAndValidateToolsConfig({
1971
+ const validated = this.validateToolsConfig({
1836
1972
  ...this.config.tools,
1837
1973
  placement: nextPlacement,
1838
1974
  }, {
@@ -1840,8 +1976,8 @@ export class ToolkitCoordinator {
1840
1976
  source: "ToolkitCoordinator.updateToolsPlacement",
1841
1977
  toolRegistry: this.toolRegistry,
1842
1978
  });
1843
- validated.config.placement = nextPlacement;
1844
- this.config.tools = validated.config;
1979
+ validated.placement = nextPlacement;
1980
+ this.config.tools = validated;
1845
1981
  this.policyEngine.updateInputs({
1846
1982
  tools: this.config.tools,
1847
1983
  });
@@ -1867,10 +2003,19 @@ export class ToolkitCoordinator {
1867
2003
  * capability it asks about, which is indistinguishable from a student who was
1868
2004
  * properly declined — so without this, forgetting {@link updateAssessment}
1869
2005
  * presents as an accommodation that silently never appears.
2006
+ *
2007
+ * A coordinator built with `assessmentOptional` reports only while PNP
2008
+ * enforcement is explicitly `"on"`. Its toolkit binds whatever assessment the
2009
+ * host gave, usually none, and a host that passes no profile and leaves
2010
+ * enforcement unset or `"off"` has asked for no accommodation.
1870
2011
  */
1871
2012
  decideFeaturePolicy(featureId) {
1872
2013
  const decision = this.policyEngine.decideFeature(featureId);
1873
- if (!decision.assessmentBound && !this.reportedUnboundFeaturePolicy) {
2014
+ const unboundIsMisconfigured = this.config.assessmentOptional !== true ||
2015
+ this.pnpEnforcementOverride === "on";
2016
+ if (!decision.assessmentBound &&
2017
+ unboundIsMisconfigured &&
2018
+ !this.reportedUnboundFeaturePolicy) {
1874
2019
  this.reportedUnboundFeaturePolicy = true;
1875
2020
  console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so every capability will be declined for want of a profile to read. Call updateAssessment(...) with the assessment (its personalNeedsProfile, settings.districtPolicy and settings.testAdministration are what policy reads) before relying on any accommodation. Reported once per coordinator.`);
1876
2021
  }
@@ -2109,37 +2254,80 @@ export class ToolkitCoordinator {
2109
2254
  }
2110
2255
  /**
2111
2256
  * Apply tool configuration changes to underlying services.
2112
- * Called after updateToolConfig().
2257
+ * Called after updateToolConfig() with the tool's config before the update.
2113
2258
  */
2114
- _applyToolConfigChange(toolId, _updates) {
2259
+ _applyToolConfigChange(toolId, previousConfig) {
2115
2260
  if (this.disposePromise !== null)
2116
2261
  return;
2117
2262
  // Apply configuration changes based on tool
2118
2263
  switch (toolId) {
2119
- case "textToSpeech": {
2120
- const reconfigurePromise = this._reconfigureTTSProvider();
2121
- this.ttsReconfigurePromise = reconfigurePromise;
2122
- void reconfigurePromise.finally(() => {
2123
- if (this.ttsReconfigurePromise === reconfigurePromise) {
2124
- this.ttsReconfigurePromise = undefined;
2125
- }
2126
- });
2127
- void reconfigurePromise.then(async () => {
2128
- if (this.disposePromise !== null)
2129
- return;
2130
- const ttsConfig = this.getTTSConfigFromProviders();
2131
- if (!this.lazyInit && ttsConfig?.enabled !== false) {
2132
- await this.ensureTTSReady(ttsConfig);
2133
- }
2134
- });
2264
+ case "textToSpeech":
2265
+ this.scheduleTTSReconfigure();
2135
2266
  break;
2136
- }
2137
2267
  case "answerEliminator":
2138
2268
  // Future: Could notify answer eliminator tools of strategy change
2139
2269
  break;
2140
- // Add cases for other tools as needed
2270
+ default:
2271
+ this._registerChangedToolProvider(toolId, previousConfig);
2141
2272
  }
2142
2273
  }
2274
+ /**
2275
+ * Register the provider a tool's updated config names, and unregister the
2276
+ * one its previous config named when the provider id changed, as
2277
+ * {@link _reconfigureTTSProvider} does for `tts`. Registration completes
2278
+ * before this returns, so a check on the policy change the update dispatched
2279
+ * finds the new provider.
2280
+ */
2281
+ _registerChangedToolProvider(toolId, previousConfig) {
2282
+ const registration = this.getProviderDescriptorTools().find((tool) => tool.toolId === toolId);
2283
+ if (!registration)
2284
+ return;
2285
+ void this.registerProviderFromTool(registration);
2286
+ const resolveQuietly = (config) => {
2287
+ try {
2288
+ return resolveToolProviderId(registration, config);
2289
+ }
2290
+ catch {
2291
+ return null;
2292
+ }
2293
+ };
2294
+ const previousId = resolveQuietly(previousConfig);
2295
+ const nextId = resolveQuietly(this.getToolConfig(toolId) || undefined);
2296
+ if (!previousId ||
2297
+ previousId === nextId ||
2298
+ !this.toolProviderRegistry.has(previousId)) {
2299
+ return;
2300
+ }
2301
+ void this.toolProviderRegistry.unregister(previousId).catch((err) => {
2302
+ console.warn(`[ToolkitCoordinator] Failed to unregister provider "${previousId}":`, err);
2303
+ });
2304
+ }
2305
+ /**
2306
+ * Re-register the `tts` provider for the current config and registry, then
2307
+ * re-initialize unless initialization is lazy. An initialization already
2308
+ * running finishes first: it would otherwise mark its provider ready after
2309
+ * the reset.
2310
+ */
2311
+ scheduleTTSReconfigure() {
2312
+ const inFlight = this.ttsInitPromise;
2313
+ const reconfigurePromise = inFlight
2314
+ ? inFlight.catch(() => { }).then(() => this._reconfigureTTSProvider())
2315
+ : this._reconfigureTTSProvider();
2316
+ this.ttsReconfigurePromise = reconfigurePromise;
2317
+ void reconfigurePromise.finally(() => {
2318
+ if (this.ttsReconfigurePromise === reconfigurePromise) {
2319
+ this.ttsReconfigurePromise = undefined;
2320
+ }
2321
+ });
2322
+ void reconfigurePromise.then(async () => {
2323
+ if (this.disposePromise !== null)
2324
+ return;
2325
+ const ttsConfig = this.getTTSConfigFromProviders();
2326
+ if (!this.lazyInit && ttsConfig?.enabled !== false) {
2327
+ await this.ensureTTSReady(ttsConfig);
2328
+ }
2329
+ });
2330
+ }
2143
2331
  async _reconfigureTTSProvider() {
2144
2332
  this.ttsInitialized = false;
2145
2333
  this.ttsInitPromise = undefined;
@@ -68,4 +68,15 @@ export declare function catalogOwnerContextFor(owner: CatalogOwnerIdentity): Cat
68
68
  * register the owner once through `AccessibilityCatalogResolver.registerOwner`.
69
69
  */
70
70
  export declare function collectOwnerCatalogRegistrations(entity: CatalogSourceEntity | null | undefined, owner: CatalogOwnerIdentity): CatalogOwnerRegistrationEntry[];
71
+ /**
72
+ * The catalogs an owner registration of `entity` would file, as a string equal
73
+ * for any two entities that register the same catalogs. A shell holding new
74
+ * content under an unchanged identity compares it to decide whether the runtime
75
+ * has anything new to register. It is derived from the registration walk, so it
76
+ * cannot disagree with the registration about what counts as a catalog.
77
+ *
78
+ * `null` when the catalogs do not serialize: nothing can be concluded, and the
79
+ * caller registers again.
80
+ */
81
+ export declare function catalogSourceSignature(entity: CatalogSourceEntity | null | undefined, kind: CatalogOwnerIdentity["kind"]): string | null;
71
82
  export {};
@@ -62,3 +62,28 @@ export function collectOwnerCatalogRegistrations(entity, owner) {
62
62
  }
63
63
  return registrations;
64
64
  }
65
+ /**
66
+ * The catalogs an owner registration of `entity` would file, as a string equal
67
+ * for any two entities that register the same catalogs. A shell holding new
68
+ * content under an unchanged identity compares it to decide whether the runtime
69
+ * has anything new to register. It is derived from the registration walk, so it
70
+ * cannot disagree with the registration about what counts as a catalog.
71
+ *
72
+ * `null` when the catalogs do not serialize: nothing can be concluded, and the
73
+ * caller registers again.
74
+ */
75
+ export function catalogSourceSignature(entity, kind) {
76
+ const registrations = collectOwnerCatalogRegistrations(entity, {
77
+ kind,
78
+ itemId: "",
79
+ });
80
+ try {
81
+ return JSON.stringify(registrations.map((registration) => [
82
+ registration.context.modelId ?? null,
83
+ registration.catalogs,
84
+ ]));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
@@ -1,8 +1,8 @@
1
- import type { CanonicalToolsConfig } from "./tools-config-normalizer.js";
1
+ import type { ToolsConfigInput } from "./tools-config-normalizer.js";
2
2
  import type { ToolRegistry } from "./ToolRegistry.js";
3
3
  import { type ToolConfigStrictness, type ToolConfigValidationResult } from "./tool-config-validation.js";
4
4
  export interface CreateToolsConfigArgs {
5
- tools?: Partial<CanonicalToolsConfig> | null;
5
+ tools?: ToolsConfigInput | null;
6
6
  toolRegistry?: ToolRegistry | null;
7
7
  strictness?: ToolConfigStrictness;
8
8
  source?: string;
@@ -13,7 +13,14 @@ export type FrameworkErrorKind = "tool-config" | "runtime-init" | "runtime-dispo
13
13
  * key still resolves through the English fallback chain, so the player renders
14
14
  * — in the wrong language, which a host wants to know about.
15
15
  */
16
- | "i18n-locale-load" | "unknown";
16
+ | "i18n-locale-load"
17
+ /**
18
+ * A section player could not register the elements its items need before
19
+ * mounting them: a renderable failed the config contract, an element bundle
20
+ * or module failed to load, or a `preloaded` page lacks a registration the
21
+ * content names. Not recoverable — the section's items stay unmounted.
22
+ */
23
+ | "element-preload" | "unknown";
17
24
  export type FrameworkErrorSeverity = "warning" | "error";
18
25
  export interface FrameworkErrorModel {
19
26
  kind: FrameworkErrorKind;