@pie-players/pie-assessment-toolkit 1.0.0-beta.7 → 1.0.0-beta.9

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 (46) hide show
  1. package/custom-elements.json +7 -0
  2. package/dist/components/ItemScope.custom-element.js +1 -1
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-bsww9e8f.js +4 -0
  7. package/dist/components/chunks/ItemToolBar-eg8jarh0.js +2 -0
  8. package/dist/components/chunks/{ItemToolBar-pxan157g.js → ItemToolBar-gsb96731.js} +2 -2
  9. package/dist/components/chunks/ItemToolBar-na4e5vwt.js +2 -0
  10. package/dist/components/chunks/{ItemToolBar-xwjrpdmz.js → ItemToolBar-wcw4yxjy.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-zvy0hskj.js +40 -0
  12. package/dist/components/chunks/sre-engine-wazxma67.js +1 -0
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/runtime/SectionControllerBinding.d.ts +16 -5
  16. package/dist/runtime/SectionControllerBinding.js +14 -6
  17. package/dist/runtime/registration-events.d.ts +31 -4
  18. package/dist/runtime/registration-events.js +20 -1
  19. package/dist/runtime/shell-event-bridge.d.ts +7 -1
  20. package/dist/runtime/shell-event-bridge.js +44 -1
  21. package/dist/runtime/shell-scope.d.ts +2 -2
  22. package/dist/runtime/shell-scope.js +14 -5
  23. package/dist/services/SSMLExtractor.js +8 -23
  24. package/dist/services/ToolRegistry.d.ts +11 -3
  25. package/dist/services/ToolRegistry.js +8 -4
  26. package/dist/services/ToolkitCoordinator.d.ts +12 -47
  27. package/dist/services/ToolkitCoordinator.js +45 -284
  28. package/dist/services/section-controller-types.d.ts +71 -20
  29. package/dist/services/tool-config-validation.d.ts +1 -1
  30. package/dist/services/tool-context.js +94 -11
  31. package/dist/services/tool-icons.js +2 -0
  32. package/dist/services/tool-provider-defaults.d.ts +13 -0
  33. package/dist/services/tool-provider-defaults.js +20 -0
  34. package/dist/services/tts/browser-provider.js +3 -1
  35. package/dist/services/tts/tts-lifecycle.d.ts +142 -0
  36. package/dist/services/tts/tts-lifecycle.js +341 -0
  37. package/dist/tools/content-surfaces.d.ts +35 -0
  38. package/dist/tools/content-surfaces.js +35 -0
  39. package/dist/tools/registration.d.ts +2 -0
  40. package/dist/tools/registration.js +5 -0
  41. package/package.json +8 -8
  42. package/dist/components/chunks/ItemToolBar-4vq9nbxz.js +0 -2
  43. package/dist/components/chunks/ItemToolBar-f971t0cw.js +0 -4
  44. package/dist/components/chunks/ItemToolBar-n05xjkp8.js +0 -2
  45. package/dist/components/chunks/ItemToolBar-rgmvw9sm.js +0 -40
  46. package/dist/components/chunks/sre-engine-dq0w45fv.js +0 -1
@@ -31,9 +31,9 @@ import { FrameworkErrorBus, } from "./framework-error-bus.js";
31
31
  import { HighlightCoordinator } from "./HighlightCoordinator.js";
32
32
  import { ToolCoordinator } from "./ToolCoordinator.js";
33
33
  import { TTSService } from "./TTSService.js";
34
- import { BrowserTTSProvider, browserFallbackConfig, } from "./tts/browser-provider.js";
35
- import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
36
34
  import { ToolProviderRegistry } from "./tool-providers/index.js";
35
+ import { TtsLifecycle, TTS_TOOL_ID, deepMergeMathSpeech, } from "./tts/tts-lifecycle.js";
36
+ import { DEFAULT_TOOL_PROVIDERS } from "./tool-provider-defaults.js";
37
37
  import { ToolRegistry } from "./ToolRegistry.js";
38
38
  import { ToolRequestRegistry } from "./tool-request.js";
39
39
  import { ToolPolicyEngine, } from "../policy/engine.js";
@@ -52,13 +52,11 @@ class SectionControllerRetiredError extends Error {
52
52
  this.name = "SectionControllerRetiredError";
53
53
  }
54
54
  }
55
- const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
56
55
  const mergeToolConfigUpdate = (toolId, current, updates) => {
57
56
  const next = { ...current, ...updates };
58
- if (toolId === "textToSpeech" &&
59
- isPlainRecord(current.mathSpeech) &&
60
- isPlainRecord(updates.mathSpeech)) {
61
- next.mathSpeech = { ...current.mathSpeech, ...updates.mathSpeech };
57
+ const deepMathSpeech = deepMergeMathSpeech(toolId, current, updates);
58
+ if (deepMathSpeech) {
59
+ next.mathSpeech = deepMathSpeech;
62
60
  }
63
61
  return next;
64
62
  };
@@ -136,11 +134,7 @@ export class ToolkitCoordinator {
136
134
  hooks;
137
135
  lazyInit;
138
136
  /** Track TTS initialization state */
139
- ttsInitialized = false;
140
- /** Text-to-speech failed to start and policy does not grant it. */
141
- ttsDegraded = false;
142
- ttsInitPromise;
143
- ttsReconfigurePromise;
137
+ tts;
144
138
  /**
145
139
  * The loader ran, whether or not it returned state. A failed load is
146
140
  * reported once and the coordinator runs without saved tool state.
@@ -243,18 +237,6 @@ export class ToolkitCoordinator {
243
237
  * lifetime for no new information.
244
238
  */
245
239
  reportedUnboundFeaturePolicy = false;
246
- /**
247
- * Whether {@link _initializeTTS} has already reported a non-browser backend
248
- * falling back to browser speech because no text-to-speech provider is registered.
249
- * Once per coordinator: a text-to-speech config change re-runs
250
- * initialization, and the missing provider is the same gap each time.
251
- */
252
- reportedMissingTTSProvider = false;
253
- /**
254
- * The backend of a fallback {@link reportMissingTTSProvider} held back while
255
- * the registry was unsettled, reported if the bound toolkit has no registry.
256
- */
257
- heldMissingTTSProviderBackend = null;
258
240
  /**
259
241
  * Whether {@link validateToolsConfig} has already reported validating with
260
242
  * no registry (`tools.registryUnavailable`). Once per coordinator: every
@@ -278,13 +260,7 @@ export class ToolkitCoordinator {
278
260
  toolRegistry,
279
261
  });
280
262
  const defaultProviders = {
281
- textToSpeech: {
282
- enabled: true,
283
- backend: "browser",
284
- },
285
- annotationToolbar: {
286
- enabled: true,
287
- },
263
+ ...DEFAULT_TOOL_PROVIDERS,
288
264
  };
289
265
  return {
290
266
  ...config,
@@ -358,7 +334,27 @@ export class ToolkitCoordinator {
358
334
  // A caller that speaks without `ensureTTSReady()` first, such as selection
359
335
  // read-aloud, starts the service instead of finding it uninitialized.
360
336
  this.ttsService.setReadinessGate(() => this.ensureTTSReady());
361
- this.ttsService.setMathSpeechSource(() => buildRuntimeTTSConfig(resolveTTSRuntimeSettings(this.resolveTTSToolConfig())).providerOptions?.mathSpeech);
337
+ this.tts = new TtsLifecycle({
338
+ assertNotDisposed: () => this.assertNotDisposed(),
339
+ isDisposalStarted: () => this.disposePromise !== null,
340
+ isDisposalError: (error) => error instanceof ToolkitCoordinatorDisposedError,
341
+ emitTelemetry: (eventName, payload) => this.emitTelemetry(eventName, payload),
342
+ hooks: this.hooks,
343
+ toolProviderRegistry: this.toolProviderRegistry,
344
+ initializeProvider: (toolId, fallbackFollows) => this.initializeProvider(toolId, fallbackFollows),
345
+ ttsService: this.ttsService,
346
+ catalogResolver: this.catalogResolver,
347
+ clearDegradedTool: (toolId) => void this.degradedTools.delete(toolId),
348
+ reportStartFailure: (error, context, toolIds) => this.reportStartFailure(error, context, toolIds),
349
+ isToolGranted: (toolId) => this.isToolGranted(toolId),
350
+ lazyInit: this.lazyInit,
351
+ isToolRegistryPending: () => this.toolRegistryPending(),
352
+ providersConfig: () => this.config.tools?.providers,
353
+ getProviderDescriptorTools: () => this.getProviderDescriptorTools(),
354
+ registerProviderFromTool: (tool, replace) => this.registerProviderFromTool(tool, replace),
355
+ notifyReadyChange: () => this.notifyReadyChange(),
356
+ });
357
+ this.ttsService.setMathSpeechSource(() => this.tts.mathSpeechRuntimeOptions());
362
358
  this.setupStatePersistenceHooks();
363
359
  // The unified ToolPolicyEngine, seeded with the validated tools config.
364
360
  // The assessment starts `null` and items register their settings as they
@@ -724,17 +720,6 @@ export class ToolkitCoordinator {
724
720
  toolRegistryPending() {
725
721
  return !this.toolRegistrySettled && !this.toolRegistryAbsent;
726
722
  }
727
- reportMissingTTSProvider(backend) {
728
- // The toolkit this coordinator binds to may still supply the provider.
729
- if (this.toolRegistryPending()) {
730
- this.heldMissingTTSProviderBackend = backend;
731
- return;
732
- }
733
- if (this.reportedMissingTTSProvider)
734
- return;
735
- this.reportedMissingTTSProvider = true;
736
- console.warn(`[ToolkitCoordinator] Text-to-speech is configured for the "${backend}" backend but falls back to browser speech: no "textToSpeech" 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.`);
737
- }
738
723
  /**
739
724
  * Take the registry of the toolkit this coordinator is bound to, when the host
740
725
  * constructed it without one: the config is validated against it, its
@@ -751,8 +736,6 @@ export class ToolkitCoordinator {
751
736
  return false;
752
737
  if (!registry && this.toolRegistryAbsent)
753
738
  return false;
754
- const heldBackend = this.heldMissingTTSProviderBackend;
755
- this.heldMissingTTSProviderBackend = null;
756
739
  const strictness = this.config.toolConfigStrictness ?? "error";
757
740
  const source = "ToolkitCoordinator.adoptToolRegistry";
758
741
  if (!registry) {
@@ -762,13 +745,13 @@ export class ToolkitCoordinator {
762
745
  source,
763
746
  toolRegistry: this.toolRegistry,
764
747
  });
765
- if (heldBackend)
766
- this.reportMissingTTSProvider(heldBackend);
748
+ this.tts.reportHeldMissingProvider();
767
749
  return false;
768
750
  }
769
751
  this.toolRegistrySettled = true;
770
752
  this.toolRegistry = registry;
771
753
  this.config.toolRegistry = registry;
754
+ this.tts.discardHeldMissingProvider();
772
755
  try {
773
756
  // Warn at most: the config was accepted at construction, and a binding
774
757
  // toolkit has no caller to throw to.
@@ -782,16 +765,14 @@ export class ToolkitCoordinator {
782
765
  console.warn("[ToolkitCoordinator] Tool config does not validate against the adopted tool registry:", err);
783
766
  }
784
767
  this.policyEngine.replaceToolRegistry(registry, this.config.tools);
785
- const ttsStarted = this.ttsInitialized ||
786
- this.ttsInitPromise !== undefined ||
787
- this.ttsReconfigurePromise !== undefined;
768
+ const ttsStarted = this.tts.isStarted();
788
769
  for (const tool of this.getProviderDescriptorTools()) {
789
- if (ttsStarted && tool.toolId === "textToSpeech")
770
+ if (ttsStarted && this.tts.ownsTool(tool.toolId))
790
771
  continue;
791
772
  void this.registerProviderFromTool(tool);
792
773
  }
793
774
  if (ttsStarted)
794
- this.scheduleTTSReconfigure();
775
+ this.tts.scheduleReconfigure();
795
776
  return true;
796
777
  }
797
778
  _registerToolProviders() {
@@ -1738,159 +1719,19 @@ export class ToolkitCoordinator {
1738
1719
  if (this.coordinatorReadyPromise) {
1739
1720
  pending.add(this.coordinatorReadyPromise);
1740
1721
  }
1741
- if (this.ttsInitPromise)
1742
- pending.add(this.ttsInitPromise);
1743
- if (this.ttsReconfigurePromise) {
1744
- pending.add(this.ttsReconfigurePromise);
1745
- }
1722
+ this.tts.flushInitPromises(pending);
1746
1723
  for (const promise of this.providerInitPromises.values()) {
1747
1724
  pending.add(promise);
1748
1725
  }
1749
1726
  await settleAll(pending);
1750
1727
  }
1751
1728
  /**
1752
- * Initialize the TTS service from the `textToSpeech` tool config.
1729
+ * Initialize the TTS service from the `textToSpeech` tool config. The
1730
+ * lifecycle itself lives in `services/tts/tts-lifecycle.ts`; the coordinator
1731
+ * keeps the public method hosts call.
1753
1732
  */
1754
1733
  async ensureTTSReady() {
1755
- this.assertNotDisposed();
1756
- await this.waitForPendingTTSReconfigure();
1757
- this.assertNotDisposed();
1758
- if (this.ttsInitialized)
1759
- return;
1760
- if (this.ttsInitPromise)
1761
- return this.ttsInitPromise;
1762
- this.ttsInitPromise = this._initializeTTS()
1763
- .then(() => this.assertNotDisposed())
1764
- .finally(() => {
1765
- this.ttsInitPromise = undefined;
1766
- this.notifyReadyChange();
1767
- });
1768
- return this.ttsInitPromise;
1769
- }
1770
- async waitForPendingTTSReconfigure() {
1771
- let pending = this.ttsReconfigurePromise;
1772
- while (pending) {
1773
- await pending;
1774
- pending = this.ttsReconfigurePromise;
1775
- }
1776
- }
1777
- async _initializeTTS() {
1778
- if (this.ttsInitialized)
1779
- return;
1780
- const resolvedToolConfig = this.resolveTTSToolConfig();
1781
- const runtimeSettings = resolveTTSRuntimeSettings(resolvedToolConfig);
1782
- const resolvedBackend = resolveTTSBackend(runtimeSettings);
1783
- const runtimeTTSConfig = buildRuntimeTTSConfig(runtimeSettings);
1784
- await this.hooks.onBeforeTTSInit?.({
1785
- phase: "tts-init",
1786
- details: {
1787
- backend: resolvedBackend,
1788
- },
1789
- });
1790
- this.assertNotDisposed();
1791
- await this.emitTelemetry("pie-toolkit-tts-init-start", {
1792
- backend: resolvedBackend,
1793
- });
1794
- await this.emitTelemetry("pie-tool-init-start", {
1795
- toolId: "textToSpeech",
1796
- operation: "tts-init",
1797
- backend: resolvedBackend,
1798
- });
1799
- if (this.toolProviderRegistry.has("textToSpeech")) {
1800
- try {
1801
- // Browser speech follows a server provider that fails to start.
1802
- const ttsProvider = await this.initializeProvider("textToSpeech", true);
1803
- const providerInstance = await ttsProvider.createInstance();
1804
- await this.initializeTTSService(providerInstance, runtimeTTSConfig);
1805
- await this.emitTelemetry("pie-toolkit-tts-init-success", {
1806
- provider: "registry",
1807
- });
1808
- await this.emitTelemetry("pie-tool-init-success", {
1809
- toolId: "textToSpeech",
1810
- operation: "tts-init",
1811
- backend: resolvedBackend,
1812
- provider: "registry",
1813
- });
1814
- return;
1815
- }
1816
- catch (error) {
1817
- if (error instanceof ToolkitCoordinatorDisposedError)
1818
- throw error;
1819
- const normalized = error instanceof Error ? error : new Error(String(error));
1820
- await this.emitTelemetry("pie-tool-init-fallback", {
1821
- toolId: "textToSpeech",
1822
- operation: "tts-init",
1823
- backend: resolvedBackend,
1824
- fromProvider: "registry",
1825
- toProvider: "browser",
1826
- reason: normalized.message,
1827
- });
1828
- console.warn("[ToolkitCoordinator] Failed to initialize TTS via registry, falling back to browser provider:", normalized);
1829
- }
1830
- }
1831
- else if (resolvedBackend !== "browser") {
1832
- this.reportMissingTTSProvider(resolvedBackend);
1833
- }
1834
- // Browser speech, configured or standing in for a backend that has no
1835
- // provider or failed to start.
1836
- const provider = new BrowserTTSProvider();
1837
- try {
1838
- await this.initializeTTSService(provider, resolvedBackend === "browser"
1839
- ? runtimeTTSConfig
1840
- : browserFallbackConfig(runtimeTTSConfig));
1841
- await this.emitTelemetry("pie-toolkit-tts-init-success", {
1842
- provider: "browser-fallback",
1843
- });
1844
- await this.emitTelemetry("pie-tool-init-success", {
1845
- toolId: "textToSpeech",
1846
- operation: "tts-init",
1847
- backend: resolvedBackend,
1848
- provider: "browser-fallback",
1849
- });
1850
- }
1851
- catch (error) {
1852
- if (error instanceof ToolkitCoordinatorDisposedError)
1853
- throw error;
1854
- const normalized = error instanceof Error ? error : new Error(String(error));
1855
- this.reportStartFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
1856
- await this.emitTelemetry("pie-toolkit-tts-init-error", {
1857
- message: normalized.message,
1858
- });
1859
- await this.emitTelemetry("pie-tool-init-error", {
1860
- toolId: "textToSpeech",
1861
- operation: "tts-init",
1862
- backend: resolvedBackend,
1863
- errorType: "TTSInitError",
1864
- message: normalized.message,
1865
- });
1866
- throw normalized;
1867
- }
1868
- }
1869
- resolveTTSToolConfig() {
1870
- return this.getTTSConfigFromProviders() || {};
1871
- }
1872
- async initializeTTSService(provider, config) {
1873
- const nextProviderOptions = {
1874
- ...config.providerOptions,
1875
- __pieTelemetry: async (eventName, payload) => {
1876
- await this.emitTelemetry(eventName, {
1877
- toolId: "textToSpeech",
1878
- ...(payload || {}),
1879
- });
1880
- },
1881
- };
1882
- const nextConfig = {
1883
- ...config,
1884
- providerOptions: nextProviderOptions,
1885
- };
1886
- await this.ttsService.initialize(provider, nextConfig);
1887
- this.assertNotDisposed();
1888
- this.ttsService.setCatalogResolver(this.catalogResolver);
1889
- this.ttsInitialized = true;
1890
- this.ttsDegraded = false;
1891
- this.degradedTools.delete("textToSpeech");
1892
- await this.hooks.onTTSReady?.();
1893
- this.assertNotDisposed();
1734
+ return this.tts.ensureReady();
1894
1735
  }
1895
1736
  async waitUntilReady() {
1896
1737
  this.assertNotDisposed();
@@ -1901,8 +1742,8 @@ export class ToolkitCoordinator {
1901
1742
  this.coordinatorReadyPromise = (async () => {
1902
1743
  await this.ensureStateLoaded();
1903
1744
  this.assertNotDisposed();
1904
- if (this.ttsRequiredForReadiness()) {
1905
- await this.startTTSForReadiness();
1745
+ if (this.tts.requiredForReadiness()) {
1746
+ await this.tts.startForReadiness();
1906
1747
  }
1907
1748
  this.assertNotDisposed();
1908
1749
  if (!this.coordinatorReadyNotified) {
@@ -1920,39 +1761,9 @@ export class ToolkitCoordinator {
1920
1761
  }
1921
1762
  isReady() {
1922
1763
  return ((this.stateLoadSettled || !this.hooks.loadToolState) &&
1923
- (this.ttsInitialized ||
1924
- this.ttsDegraded ||
1925
- !this.ttsRequiredForReadiness()));
1926
- }
1927
- /**
1928
- * Readiness waits for text-to-speech when it is enabled and either starts
1929
- * eagerly or is granted: a granted read-aloud fails before the learner starts.
1930
- */
1931
- ttsRequiredForReadiness() {
1932
- if (this.getTTSConfigFromProviders()?.enabled === false)
1933
- return false;
1934
- return !this.lazyInit || this.isToolGranted("textToSpeech");
1935
- }
1936
- /**
1937
- * Starts text-to-speech for readiness. A failure policy does not grant leaves
1938
- * the tool degraded, reporting itself unavailable, and readiness settles; a
1939
- * granted tool's failure rejects.
1940
- */
1941
- async startTTSForReadiness() {
1942
- try {
1943
- await this.ensureTTSReady();
1944
- }
1945
- catch (error) {
1946
- if (error instanceof ToolkitCoordinatorDisposedError)
1947
- throw error;
1948
- if (this.isToolGranted("textToSpeech"))
1949
- throw error;
1950
- this.ttsDegraded = true;
1951
- this.notifyReadyChange();
1952
- }
1953
- }
1954
- getTTSConfigFromProviders() {
1955
- return this.config.tools?.providers?.textToSpeech;
1764
+ (this.tts.started ||
1765
+ this.tts.failedUngranted ||
1766
+ !this.tts.requiredForReadiness()));
1956
1767
  }
1957
1768
  assertCanonicalToolId(toolId) {
1958
1769
  if (typeof toolId !== "string" || toolId.trim().length === 0) {
@@ -2360,8 +2171,8 @@ export class ToolkitCoordinator {
2360
2171
  _applyToolConfigChange(toolId) {
2361
2172
  if (this.disposePromise !== null)
2362
2173
  return;
2363
- if (toolId === "textToSpeech") {
2364
- this.scheduleTTSReconfigure();
2174
+ if (this.tts.ownsTool(toolId)) {
2175
+ this.tts.scheduleReconfigure();
2365
2176
  return;
2366
2177
  }
2367
2178
  this._registerChangedToolProvider(toolId);
@@ -2379,54 +2190,4 @@ export class ToolkitCoordinator {
2379
2190
  this.degradedTools.delete(toolId);
2380
2191
  void this.registerProviderFromTool(registration, true);
2381
2192
  }
2382
- /**
2383
- * Re-register the text-to-speech provider for the current config and registry, then
2384
- * re-initialize unless initialization is lazy. An initialization already
2385
- * running finishes first: it would otherwise mark its provider ready after
2386
- * the reset.
2387
- */
2388
- scheduleTTSReconfigure() {
2389
- const inFlight = this.ttsInitPromise;
2390
- const reconfigurePromise = inFlight
2391
- ? inFlight.catch(() => { }).then(() => this._reconfigureTTSProvider())
2392
- : this._reconfigureTTSProvider();
2393
- this.ttsReconfigurePromise = reconfigurePromise;
2394
- void reconfigurePromise.finally(() => {
2395
- if (this.ttsReconfigurePromise === reconfigurePromise) {
2396
- this.ttsReconfigurePromise = undefined;
2397
- }
2398
- });
2399
- void reconfigurePromise
2400
- .then(async () => {
2401
- if (this.disposePromise !== null)
2402
- return;
2403
- if (this.ttsRequiredForReadiness())
2404
- await this.startTTSForReadiness();
2405
- })
2406
- // Reported where it failed; a granted failure has nowhere else to go.
2407
- .catch(() => { });
2408
- }
2409
- async _reconfigureTTSProvider() {
2410
- this.ttsInitialized = false;
2411
- this.ttsDegraded = false;
2412
- this.degradedTools.delete("textToSpeech");
2413
- this.ttsInitPromise = undefined;
2414
- this.notifyReadyChange();
2415
- try {
2416
- // The next speak waits on readiness, which starts the new provider.
2417
- this.ttsService.releaseProvider();
2418
- }
2419
- catch {
2420
- // noop: release best effort
2421
- }
2422
- if (this.toolProviderRegistry.has("textToSpeech")) {
2423
- await this.toolProviderRegistry.unregister("textToSpeech");
2424
- }
2425
- if (this.disposePromise !== null)
2426
- return;
2427
- const ttsRegistration = this.getProviderDescriptorTools().find((tool) => tool.toolId === "textToSpeech");
2428
- if (!ttsRegistration)
2429
- return;
2430
- await this.registerProviderFromTool(ttsRegistration);
2431
- }
2432
2193
  }
@@ -1,4 +1,4 @@
1
- import type { FormativeFeedbackReveal, FormativeMasteryRollup, FormativeSectionProjection, FormativeTryOutcome } from "@pie-players/pie-players-shared/formative";
1
+ import type { FeedbackReveal, MasteryRollup, TrySectionProjection, TryOutcome } from "@pie-players/pie-players-shared/item-session-control";
2
2
  import type { MediaTimeSource, TimedMediaDegradation, TimedMediaSectionProjection, TimedMediaValidationError } from "@pie-players/pie-players-shared/timed-media";
3
3
  import type { SessionCommitReason } from "@pie-players/pie-players-shared/pie";
4
4
  import type { SectionControllerSessionState } from "@pie-players/pie-players-shared/types";
@@ -175,12 +175,12 @@ export type SectionControllerSectionErrorEvent = SectionControllerEventBase & {
175
175
  contentKind?: "item" | "passage" | "rubric" | "unknown";
176
176
  currentItemIndex: number;
177
177
  };
178
- export type SectionControllerFormativeTryRecordedEvent = SectionControllerEventBase & {
179
- type: "formative-try-recorded";
178
+ export type SectionControllerItemTryRecordedEvent = SectionControllerEventBase & {
179
+ type: "item-try-recorded";
180
180
  itemId: string;
181
181
  canonicalItemId: string;
182
182
  tryCount: number;
183
- outcome: FormativeTryOutcome;
183
+ outcome: TryOutcome;
184
184
  /** Whether feedback is now on screen for this item. */
185
185
  revealed: boolean;
186
186
  currentItemIndex: number;
@@ -189,16 +189,16 @@ export type SectionControllerFormativeTryRecordedEvent = SectionControllerEventB
189
189
  * The reveal state changed without a Try being recorded: a learner dismissed
190
190
  * feedback to edit again, or a host forced a reveal or withdrew one.
191
191
  *
192
- * A Try that reveals reports through `formative-try-recorded`, which carries its
192
+ * A Try that reveals reports through `item-try-recorded`, which carries its
193
193
  * own `revealed`. This event is for every other transition.
194
194
  */
195
- export type SectionControllerFormativeRevealChangedEvent = SectionControllerEventBase & {
196
- type: "formative-reveal-changed";
195
+ export type SectionControllerItemFeedbackRevealChangedEvent = SectionControllerEventBase & {
196
+ type: "item-feedback-reveal-changed";
197
197
  itemId: string;
198
198
  canonicalItemId: string;
199
199
  revealed: boolean;
200
200
  /** Reveal level in force, when a host overrode the policy's. */
201
- feedback?: FormativeFeedbackReveal;
201
+ feedback?: FeedbackReveal;
202
202
  /** Tries already spent; none of these transitions consume one. */
203
203
  tryCount: number;
204
204
  source: "learner" | "host";
@@ -206,7 +206,7 @@ export type SectionControllerFormativeRevealChangedEvent = SectionControllerEven
206
206
  };
207
207
  export type SectionControllerSectionMasteryChangedEvent = SectionControllerEventBase & {
208
208
  type: "section-mastery-changed";
209
- mastery: FormativeMasteryRollup;
209
+ mastery: MasteryRollup;
210
210
  currentItemIndex: number;
211
211
  };
212
212
  /**
@@ -268,8 +268,28 @@ export type SectionControllerTimedMediaInvalidEvent = SectionControllerEventBase
268
268
  errors: TimedMediaValidationError[];
269
269
  currentItemIndex: number;
270
270
  };
271
- export type SectionControllerEvent = SectionControllerItemSessionDataChangedEvent | SectionControllerItemSessionMetaChangedEvent | SectionControllerItemSelectedEvent | SectionControllerSectionNavigationChangedEvent | SectionControllerSectionSessionAppliedEvent | SectionControllerContentLoadedEvent | SectionControllerItemPlayerErrorEvent | SectionControllerItemCompleteChangedEvent | SectionControllerSectionLoadingCompleteEvent | SectionControllerSectionItemsCompleteChangedEvent | SectionControllerSectionErrorEvent | SectionControllerFormativeTryRecordedEvent | SectionControllerFormativeRevealChangedEvent | SectionControllerSectionMasteryChangedEvent | SectionControllerTimedMediaCueChangedEvent | SectionControllerTimedMediaAudioStartedEvent | SectionControllerTimedMediaPolicyDegradedEvent | SectionControllerTimedMediaInvalidEvent;
271
+ export type SectionControllerEvent = SectionControllerItemSessionDataChangedEvent | SectionControllerItemSessionMetaChangedEvent | SectionControllerItemSelectedEvent | SectionControllerSectionNavigationChangedEvent | SectionControllerSectionSessionAppliedEvent | SectionControllerContentLoadedEvent | SectionControllerItemPlayerErrorEvent | SectionControllerItemCompleteChangedEvent | SectionControllerSectionLoadingCompleteEvent | SectionControllerSectionItemsCompleteChangedEvent | SectionControllerSectionErrorEvent | SectionControllerItemTryRecordedEvent | SectionControllerItemFeedbackRevealChangedEvent | SectionControllerSectionMasteryChangedEvent | SectionControllerTimedMediaCueChangedEvent | SectionControllerTimedMediaAudioStartedEvent | SectionControllerTimedMediaPolicyDegradedEvent | SectionControllerTimedMediaInvalidEvent;
272
272
  export type SectionControllerEventType = SectionControllerEvent["type"];
273
+ /** One item's part of a {@link SectionScore}. */
274
+ export type SectionItemScore = {
275
+ canonicalItemId: string;
276
+ /**
277
+ * The item's element outcomes, one slot per scored model as
278
+ * `pie-item-player.provideScore()` returns them. `null` while the item is not
279
+ * rendered, or when it has no scorable model.
280
+ */
281
+ outcomes: Array<Record<string, unknown> | undefined> | null;
282
+ };
283
+ /** The section's browser score, from {@link SectionControllerHandle.score}. */
284
+ export type SectionScore = {
285
+ /** Every item in the section, in section order. */
286
+ items: SectionItemScore[];
287
+ };
288
+ /**
289
+ * Scores one rendered item in the browser: its element outcomes, `false` when
290
+ * the item has no scorable model, or `null` when its player has not mounted.
291
+ */
292
+ export type SectionItemScorer = (canonicalItemId: string) => Promise<false | null | Array<Record<string, unknown> | undefined>>;
273
293
  /**
274
294
  * Host-facing controller for a single section cohort.
275
295
  *
@@ -422,10 +442,30 @@ export interface SectionControllerHandle {
422
442
  * those boundaries.
423
443
  */
424
444
  setPendingSessionCommit?(commit: (() => void) | null): void;
445
+ /**
446
+ * Commit every element's pending response to the section session, so the
447
+ * next `getSession()` holds it. For a host about to read or submit the
448
+ * session; the controller already commits before its own boundaries.
449
+ */
450
+ commitPendingSessions?(): void;
451
+ /**
452
+ * Register how the controller scores a rendered item. The controller is
453
+ * DOM-free, so the layout that renders the item players supplies it, as with
454
+ * `setPendingSessionCommit`. The latest registration wins; the returned
455
+ * release unregisters this scorer and leaves a later one in place, so two
456
+ * layouts on one controller do not unregister each other.
457
+ */
458
+ registerItemScorer?(scorer: SectionItemScorer): () => void;
459
+ /**
460
+ * Score the section in the browser: each item's element outcomes, in section
461
+ * order. An item that is not rendered, or has no scorable model, scores
462
+ * `null`.
463
+ */
464
+ score?(): Promise<SectionScore>;
425
465
  /**
426
466
  * Record one Try for an item: derive correctness from the element outcomes,
427
467
  * increment the Try count, and reveal feedback if the item's resolved
428
- * formative policy says so.
468
+ * item session control policy says so.
429
469
  *
430
470
  * `outcomes` is what `pie-item-player.provideScore()` returned. The caller
431
471
  * does not interpret it — correctness derivation belongs to the controller so
@@ -434,18 +474,29 @@ export interface SectionControllerHandle {
434
474
  * No-op when the item's policy is disabled, when feedback is already
435
475
  * revealed, or when Tries are spent, which makes a double submit safe.
436
476
  */
437
- recordFormativeTry?(args: {
477
+ recordTry?(args: {
438
478
  itemId: string;
439
479
  outcomes?: unknown[];
440
480
  }): void;
481
+ /**
482
+ * Score a rendered item through the registered item scorer and record the
483
+ * Try from its outcomes: Check answer, for a host that hides the built-in
484
+ * control (`policies.checkControl.enabled: false`). Rejects, spending no
485
+ * Try, when no layout has registered a scorer, the item's player has not
486
+ * mounted, or scoring throws. A call while a check of the same item is
487
+ * pending is dropped.
488
+ */
489
+ checkItem?(args: {
490
+ itemId: string;
491
+ }): Promise<void>;
441
492
  /**
442
493
  * Dismiss a revealed item's feedback and reopen it for editing. Withdrawing
443
494
  * the reveal is what withdraws the `mode: "evaluate"` projection.
444
495
  *
445
496
  * The learner action, so it respects the Try budget: no-op when nothing is
446
- * revealed or Tries are spent. Use `hideFormativeItem` for host authority.
497
+ * revealed or Tries are spent. Use `hideItemFeedback` for host authority.
447
498
  */
448
- retryFormativeItem?(args: {
499
+ retryItem?(args: {
449
500
  itemId: string;
450
501
  }): void;
451
502
  /**
@@ -457,27 +508,27 @@ export interface SectionControllerHandle {
457
508
  * `feedback` is stated rather than taken from the policy, because a reveal
458
509
  * under `feedback: "none"` would project nothing.
459
510
  */
460
- revealFormativeItem?(args: {
511
+ revealItemFeedback?(args: {
461
512
  itemId: string;
462
513
  feedback: "correctness" | "solution";
463
514
  }): void;
464
515
  /**
465
- * Withdraw a reveal on host authority. Unlike `retryFormativeItem` this
516
+ * Withdraw a reveal on host authority. Unlike `retryItem` this
466
517
  * ignores the Try budget, so a host that revealed a spent item can put it
467
518
  * back.
468
519
  */
469
- hideFormativeItem?(args: {
520
+ hideItemFeedback?(args: {
470
521
  itemId: string;
471
522
  }): void;
472
523
  /**
473
- * The section's formative projection — resolved policies, per-item state, and
524
+ * The section's Try projection — resolved policies, per-item state, and
474
525
  * the mastery rollup.
475
526
  *
476
- * `null` when no item in the section delivers formatively. The same
527
+ * `null` when no item in the section enables item session control. The same
477
528
  * projection rides on the composition model, so layouts normally read it from
478
529
  * there rather than calling this.
479
530
  */
480
- getFormativeProjection?(): FormativeSectionProjection | null;
531
+ getTryProjection?(): TrySectionProjection | null;
481
532
  /**
482
533
  * Bind the section's Media Time Source — the only way it reaches media.
483
534
  *
@@ -3,7 +3,7 @@ import type { ToolRegistry } from "./ToolRegistry.js";
3
3
  export type ToolConfigStrictness = "off" | "warn" | "error";
4
4
  export type ToolConfigDiagnosticSeverity = "warning" | "error";
5
5
  export interface ToolConfigDiagnostic {
6
- code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
6
+ code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation" | "tools.invalidProviderConfig";
7
7
  severity: ToolConfigDiagnosticSeverity;
8
8
  path: string;
9
9
  message: string;