@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 @@ import type { CatalogOwnerContext } from "./catalog-owner.js";
14
14
  import type { FrameworkErrorListener } from "./framework-error-bus.js";
15
15
  import type { FrameworkErrorModel } from "./framework-error.js";
16
16
  import type { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
17
- import type { SectionControllerHandle, SectionItemEventSubscriptionArgs, SectionScopedEventSubscriptionArgs, SectionEventSubscriptionArgs, ToolkitCoordinatorHooks, ToolkitInitStatus } from "./ToolkitCoordinator.js";
17
+ import type { SectionControllerHandle, SectionItemEventSubscriptionArgs, SectionScopedEventSubscriptionArgs, SectionEventSubscriptionArgs, ToolkitCoordinatorHooks, ToolkitInitStatus, TTSToolConfig } from "./ToolkitCoordinator.js";
18
18
  import type { ThemeConfig } from "./ThemeProvider.js";
19
19
  import type { ZIndexLayer } from "./ToolCoordinator.js";
20
20
  import type { PlaybackState, TTSConfig } from "./TTSService.js";
@@ -244,6 +244,12 @@ export interface TtsServiceApi {
244
244
  * Get currently speaking text
245
245
  */
246
246
  getCurrentText(): string | null;
247
+ /**
248
+ * Whether a catalog holds spoken content this service could speak.
249
+ * Optional: a service without one resolves a selection to the nearest
250
+ * docked catalog id.
251
+ */
252
+ hasSpokenAlternate?(catalogId: string, language?: string): boolean;
247
253
  /**
248
254
  * Subscribe to state changes
249
255
  */
@@ -421,14 +427,6 @@ export interface ToolkitCoordinatorApi {
421
427
  * Assessment identifier
422
428
  */
423
429
  readonly assessmentId: string;
424
- /**
425
- * Configuration
426
- */
427
- readonly config: {
428
- tools?: {
429
- providers?: Record<string, ToolProviderConfig | undefined>;
430
- };
431
- };
432
430
  /**
433
431
  * TTS service
434
432
  */
@@ -491,10 +489,12 @@ export interface ToolkitCoordinatorApi {
491
489
  /**
492
490
  * Get tool configuration
493
491
  */
492
+ getToolConfig(toolId: "textToSpeech"): TTSToolConfig | null;
494
493
  getToolConfig(toolId: string): ToolProviderConfig | null;
495
494
  /**
496
495
  * Update tool configuration
497
496
  */
497
+ updateToolConfig(toolId: "textToSpeech", updates: Partial<TTSToolConfig>): void;
498
498
  updateToolConfig(toolId: string, updates: Partial<ToolProviderConfig>): void;
499
499
  /**
500
500
  * Update the enabled tool list for one placement level.
@@ -167,7 +167,7 @@ export declare const QTI_STANDARD_ACCESS_FEATURES: {
167
167
  /**
168
168
  * Flat list of all standard access features for validation
169
169
  */
170
- export declare const ALL_STANDARD_ACCESS_FEATURES: ("transcript" | "braille" | "textToSpeech" | "calculator" | "graph" | "periodicTable" | "ruler" | "protractor" | "answerEliminator" | "signLanguage" | "readAloud" | "strikethrough" | "magnification" | "screenMagnifier" | "zoomable" | "highContrastDisplay" | "highContrastAudio" | "colorContrast" | "invertColors" | "displayTransformability" | "largePrint" | "fontEnlargement" | "resizeText" | "alternativeText" | "longDescription" | "describedMath" | "tactileGraphic" | "tactileObject" | "audioDescription" | "humanVoice" | "syntheticVoice" | "speechRate" | "speechVolume" | "voicePitch" | "captions" | "closedCaptions" | "openCaptions" | "subtitles" | "audioControl" | "noBackgroundAudio" | "keyboardControl" | "mouseControl" | "touchControl" | "voiceControl" | "switchControl" | "eyeGazeControl" | "singleSwitchAccess" | "stickyKeys" | "keyboardShortcuts" | "timingControl" | "unlimitedTime" | "extendedTime" | "pauseControl" | "simplifiedLanguage" | "reducedComplexity" | "structuralNavigation" | "tableOfContents" | "reducedDistraction" | "noFlashing" | "pauseAnimation" | "annotations" | "bookmarking" | "highlighting" | "guidedNavigation" | "thesaurus" | "spellingAssistance" | "grammarAssistance" | "lineSpacing" | "wordSpacing" | "letterSpacing" | "fontFamily" | "readingMask" | "readingGuide" | "readingRuler" | "wordHighlighting" | "lineHighlighting" | "focusIndicator" | "printableResource" | "nemeth" | "refreshableBraille" | "index" | "pageNavigation" | "skipContent" | "breadcrumbs" | "searchable" | "fullTextSearch" | "multilingualText" | "translatedText" | "glossary" | "signLanguageInterpretation" | "visualLanguage" | "graphingCalculator" | "formulaSheet" | "answerMasking" | "itemGlossary" | "tutorialAvailable")[];
170
+ export declare const ALL_STANDARD_ACCESS_FEATURES: ("transcript" | "braille" | "textToSpeech" | "calculator" | "graph" | "periodicTable" | "ruler" | "protractor" | "answerEliminator" | "signLanguage" | "readAloud" | "index" | "strikethrough" | "magnification" | "screenMagnifier" | "zoomable" | "highContrastDisplay" | "highContrastAudio" | "colorContrast" | "invertColors" | "displayTransformability" | "largePrint" | "fontEnlargement" | "resizeText" | "alternativeText" | "longDescription" | "describedMath" | "tactileGraphic" | "tactileObject" | "audioDescription" | "humanVoice" | "syntheticVoice" | "speechRate" | "speechVolume" | "voicePitch" | "captions" | "closedCaptions" | "openCaptions" | "subtitles" | "audioControl" | "noBackgroundAudio" | "keyboardControl" | "mouseControl" | "touchControl" | "voiceControl" | "switchControl" | "eyeGazeControl" | "singleSwitchAccess" | "stickyKeys" | "keyboardShortcuts" | "timingControl" | "unlimitedTime" | "extendedTime" | "pauseControl" | "simplifiedLanguage" | "reducedComplexity" | "structuralNavigation" | "tableOfContents" | "reducedDistraction" | "noFlashing" | "pauseAnimation" | "annotations" | "bookmarking" | "highlighting" | "guidedNavigation" | "thesaurus" | "spellingAssistance" | "grammarAssistance" | "lineSpacing" | "wordSpacing" | "letterSpacing" | "fontFamily" | "readingMask" | "readingGuide" | "readingRuler" | "wordHighlighting" | "lineHighlighting" | "focusIndicator" | "printableResource" | "nemeth" | "refreshableBraille" | "pageNavigation" | "skipContent" | "breadcrumbs" | "searchable" | "fullTextSearch" | "multilingualText" | "translatedText" | "glossary" | "signLanguageInterpretation" | "visualLanguage" | "graphingCalculator" | "formulaSheet" | "answerMasking" | "itemGlossary" | "tutorialAvailable")[];
171
171
  /**
172
172
  * Example PNP configurations for common accessibility needs
173
173
  * These are NOT official profiles but illustrative examples showing
@@ -1,5 +1,6 @@
1
1
  import type { FormativeFeedbackReveal, FormativeMasteryRollup, FormativeSectionProjection, FormativeTryOutcome } from "@pie-players/pie-players-shared/formative";
2
2
  import type { MediaTimeSource, TimedMediaDegradation, TimedMediaSectionProjection, TimedMediaValidationError } from "@pie-players/pie-players-shared/timed-media";
3
+ import type { SessionCommitReason } from "@pie-players/pie-players-shared/pie";
3
4
  import type { SectionControllerSessionState } from "@pie-players/pie-players-shared/types";
4
5
  /**
5
6
  * Re-exported so every existing import site keeps its specifier. The shape is
@@ -80,16 +81,30 @@ export type SectionControllerItemSessionDataChangedEvent = SectionControllerEven
80
81
  canonicalItemId: string;
81
82
  session: unknown;
82
83
  intent?: string;
84
+ /** The reported completion: the element's own when `elementId` is set. */
83
85
  complete?: boolean;
84
86
  component?: string;
87
+ /** The id of the element that reported, when the change names one. */
88
+ elementId?: string;
89
+ /** The controller input's `sectionId`. */
90
+ sectionId?: string;
91
+ /** Set when the change is a commit at a teardown, navigation or page-hidden seam. */
92
+ sessionCommitReason?: SessionCommitReason;
85
93
  currentItemIndex: number;
86
94
  };
87
95
  export type SectionControllerItemSessionMetaChangedEvent = SectionControllerEventBase & {
88
96
  type: "item-session-meta-changed";
89
97
  itemId: string;
90
98
  canonicalItemId: string;
99
+ /** The reported completion: the element's own when `elementId` is set. */
91
100
  complete?: boolean;
92
101
  component?: string;
102
+ /** The id of the element that reported, when the change names one. */
103
+ elementId?: string;
104
+ /** The controller input's `sectionId`. */
105
+ sectionId?: string;
106
+ /** Set when the change is a commit at a teardown, navigation or page-hidden seam. */
107
+ sessionCommitReason?: SessionCommitReason;
93
108
  currentItemIndex: number;
94
109
  };
95
110
  export type SectionControllerItemSelectedEvent = SectionControllerEventBase & {
@@ -392,6 +407,21 @@ export interface SectionControllerHandle {
392
407
  * is what the strategy load/save methods exchange.
393
408
  */
394
409
  configureSessionPersistence?(config: SectionSessionPersistenceConfig): void | Promise<void>;
410
+ /**
411
+ * Register a commit for the section's pending element sessions.
412
+ *
413
+ * A delivery element coalesces its `session-changed` dispatch, so a response
414
+ * can still be pending when the controller reaches a boundary that reads or
415
+ * discards the session - `updateInput()`, `navigateToItem()`, `persist()`.
416
+ * The controller is DOM-free, so the owner of the section's DOM supplies the
417
+ * commit and the controller calls it before those boundaries.
418
+ *
419
+ * The player that mounts the section registers this on every controller it
420
+ * creates. A host-built controller that leaves it unimplemented keeps the
421
+ * behaviour it had before the hook existed, and loses a pending response at
422
+ * those boundaries.
423
+ */
424
+ setPendingSessionCommit?(commit: (() => void) | null): void;
395
425
  /**
396
426
  * Record one Try for an item: derive correctness from the element outcomes,
397
427
  * increment the Try count, and reveal feedback if the item's resolved
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Part of PIE Assessment Toolkit.
22
22
  */
23
- import { isUnsupportedMediaAssetVersion, normalizeMediaFragment, normalizeMediaSources, trimmedOrUndefined, } from "./catalog-media.js";
23
+ import { isUnsupportedMediaAssetVersion, normalizeMediaFragment, normalizeMediaSources, trimmedOrUndefined, } from "@pie-players/pie-players-shared/media";
24
24
  /** Catalog type token for spoken alternates. Matches QTI 3's `support` value. */
25
25
  export const SPOKEN_CATALOG_TYPE = "spoken";
26
26
  /**
@@ -1,4 +1,4 @@
1
- import { type CanonicalToolsConfig } from "./tools-config-normalizer.js";
1
+ import { type CanonicalToolsConfig, type ToolsConfigInput } from "./tools-config-normalizer.js";
2
2
  import { type FrameworkErrorModel } from "./framework-error.js";
3
3
  import type { ToolRegistry } from "./ToolRegistry.js";
4
4
  export type ToolConfigStrictness = "off" | "warn" | "error";
@@ -27,4 +27,15 @@ export declare function frameworkErrorFromToolConfigValidation(args: {
27
27
  recoverable?: boolean;
28
28
  }): FrameworkErrorModel;
29
29
  export declare function normalizeToolConfigStrictness(value: unknown): ToolConfigStrictness;
30
- export declare function normalizeAndValidateToolsConfig(input?: Partial<CanonicalToolsConfig> | null, options?: ToolConfigValidationOptions): ToolConfigValidationResult;
30
+ export declare function normalizeAndValidateToolsConfig(input?: ToolsConfigInput | null, options?: ToolConfigValidationOptions): ToolConfigValidationResult;
31
+ /**
32
+ * The normalization and diagnostics of {@link normalizeAndValidateToolsConfig},
33
+ * without reporting: nothing is printed or thrown.
34
+ */
35
+ export declare function collectToolConfigDiagnostics(input: ToolsConfigInput | null | undefined, toolRegistry: ToolRegistry | null | undefined): ToolConfigValidationResult;
36
+ /**
37
+ * Report diagnostics as {@link normalizeAndValidateToolsConfig} does: a removed
38
+ * provider key throws at every strictness, `"warn"` prints every diagnostic, and
39
+ * `"error"` throws the blocking ones or, when there are none, prints the rest.
40
+ */
41
+ export declare function reportToolConfigDiagnostics(diagnostics: ToolConfigDiagnostic[], options?: Pick<ToolConfigValidationOptions, "strictness" | "source">): void;
@@ -215,15 +215,22 @@ function throwValidationError(diagnostics, source) {
215
215
  throw new Error(`[tool-config-validation:${source}] Invalid tools config:\n${formatted}`);
216
216
  }
217
217
  export function normalizeAndValidateToolsConfig(input, options = {}) {
218
- const strictness = normalizeToolConfigStrictness(options.strictness);
219
- const source = options.source ?? "tools";
220
- const registryTools = getRegistryToolMap(options.toolRegistry);
218
+ const result = collectToolConfigDiagnostics(input, options.toolRegistry);
219
+ reportToolConfigDiagnostics(result.diagnostics, options);
220
+ return result;
221
+ }
222
+ /**
223
+ * The normalization and diagnostics of {@link normalizeAndValidateToolsConfig},
224
+ * without reporting: nothing is printed or thrown.
225
+ */
226
+ export function collectToolConfigDiagnostics(input, toolRegistry) {
227
+ const registryTools = getRegistryToolMap(toolRegistry);
221
228
  const normalized = normalizeToolsConfig(input);
222
229
  const diagnostics = [];
223
230
  // Tool-id and provider checks need a registry to check against, and every
224
231
  // collector below returns early without one. Say so: this package no longer
225
232
  // falls back to a packaged registry, so a caller that used to get id
226
- // validation for free now gets none, and a silent downgrade from "your ids are
233
+ // validation for free now gets none, and a silent downgrade of "your ids are
227
234
  // valid" to "nobody looked" is the kind of change that surfaces as a typo
228
235
  // reaching a learner.
229
236
  const hasConfiguredTools = Object.values(normalized.placement).some((ids) => ids.length > 0) ||
@@ -235,7 +242,7 @@ export function normalizeAndValidateToolsConfig(input, options = {}) {
235
242
  code: "tools.registryUnavailable",
236
243
  severity: "warning",
237
244
  path: "tools",
238
- message: 'No tool registry was supplied, so tool ids, placement levels and provider config were not validated. Pass `toolRegistry` — for the packaged capability set, `createPackagedToolRegistry()` from "@pie-players/pie-default-tool-loaders".',
245
+ message: 'No tool registry was supplied, so tool ids, placement levels and provider config were not validated, and a coordinator without one registers no tool providers. Pass `toolRegistry` to the coordinator or the toolkit it is bound to — for the packaged capability set, `createPackagedToolRegistry()` from "@pie-players/pie-default-tool-loaders".',
239
246
  }));
240
247
  }
241
248
  const hasRemovedTtsKey = Object.hasOwn(normalized.providers, "tts");
@@ -258,17 +265,31 @@ export function normalizeAndValidateToolsConfig(input, options = {}) {
258
265
  collectPlacementDiagnostics(nextConfig, registryTools, diagnostics);
259
266
  collectPolicyDiagnostics(nextConfig, registryTools, diagnostics);
260
267
  collectProviderKeyDiagnostics(nextConfig, registryTools, hasRemovedTtsKey, diagnostics);
261
- if (hasRemovedTtsKey) {
262
- throwValidationError(diagnostics.filter((entry) => entry.code === "tools.removedProviderKey"), source);
268
+ return {
269
+ config: nextConfig,
270
+ diagnostics,
271
+ };
272
+ }
273
+ /**
274
+ * Report diagnostics as {@link normalizeAndValidateToolsConfig} does: a removed
275
+ * provider key throws at every strictness, `"warn"` prints every diagnostic, and
276
+ * `"error"` throws the blocking ones or, when there are none, prints the rest.
277
+ */
278
+ export function reportToolConfigDiagnostics(diagnostics, options = {}) {
279
+ const strictness = normalizeToolConfigStrictness(options.strictness);
280
+ const source = options.source ?? "tools";
281
+ const removedKeys = diagnostics.filter((entry) => entry.code === "tools.removedProviderKey");
282
+ if (removedKeys.length > 0) {
283
+ throwValidationError(removedKeys, source);
263
284
  }
264
285
  if (strictness === "warn") {
265
286
  emitWarnings(diagnostics, source);
266
287
  }
267
- // Severity decides what `strictness: "error"` rejects. Every diagnostic this
268
- // function raised was `"error"` until `tools.registryUnavailable`, which
269
- // reports that validation could not run rather than that the config is wrong —
270
- // throwing on it would turn "no registry supplied" from a host's existing,
271
- // working setup into a construction failure.
288
+ // Severity decides what `strictness: "error"` rejects. Every diagnostic was
289
+ // `"error"` until `tools.registryUnavailable`, which reports that validation
290
+ // could not run rather than that the config is wrong — throwing on it would
291
+ // turn "no registry supplied" from a host's existing, working setup into a
292
+ // construction failure.
272
293
  const blocking = diagnostics.filter((entry) => entry.severity === "error");
273
294
  if (strictness === "error" && blocking.length > 0) {
274
295
  throwValidationError(blocking, source);
@@ -276,8 +297,4 @@ export function normalizeAndValidateToolsConfig(input, options = {}) {
276
297
  if (strictness === "error" && blocking.length === 0) {
277
298
  emitWarnings(diagnostics, source);
278
299
  }
279
- return {
280
- config: nextConfig,
281
- diagnostics,
282
- };
283
300
  }
@@ -261,20 +261,35 @@ export function hasMathContent(context) {
261
261
  ];
262
262
  return textIndicators.some((pattern) => pattern.test(text));
263
263
  }
264
+ const CHOICE_INTERACTION_ELEMENTS = new Set([
265
+ "multiple-choice",
266
+ "inline-choice",
267
+ "select-text",
268
+ "ebsr",
269
+ ]);
270
+ // Content names one interaction several ways: `multiple-choice`, `pie-multiple-choice`,
271
+ // `pie-element-multiple-choice`, a runtime `--version-*` tag, or any tag the item's
272
+ // `elements` map binds to `@pie-element/multiple-choice`.
273
+ const baseElementName = (tag) => tag
274
+ .toLowerCase()
275
+ .replace(/--version-[0-9a-z-]+$/, "")
276
+ .replace(/^pie-(element-)?/, "");
277
+ function isChoiceElement(tag, elements) {
278
+ if (CHOICE_INTERACTION_ELEMENTS.has(baseElementName(tag)))
279
+ return true;
280
+ if (!elements || typeof elements !== "object")
281
+ return false;
282
+ const map = elements;
283
+ const spec = map[tag] ?? map[tag.replace(/--version-[0-9A-Za-z-]+$/, "")];
284
+ if (typeof spec !== "string")
285
+ return false;
286
+ const packageName = spec.match(/^@pie-element\/([^@]+)/)?.[1];
287
+ return !!packageName && CHOICE_INTERACTION_ELEMENTS.has(packageName);
288
+ }
264
289
  /**
265
290
  * Helper to check if context contains choice-based interactions
266
291
  */
267
292
  export function hasChoiceInteraction(context) {
268
- const interactionTypes = [
269
- "pie-multiple-choice",
270
- "pie-inline-choice",
271
- "pie-select-text",
272
- "pie-ebsr",
273
- "multiple-choice",
274
- "inline-choice",
275
- "select-text",
276
- "ebsr",
277
- ];
278
293
  if (isElementContext(context)) {
279
294
  const config = context.item.config;
280
295
  if (!config?.models)
@@ -287,10 +302,11 @@ export function hasChoiceInteraction(context) {
287
302
  if (!model)
288
303
  return false;
289
304
  const type = model.element || "";
290
- return interactionTypes.includes(type);
305
+ return isChoiceElement(type, config.elements);
291
306
  }
292
307
  if (isItemContext(context)) {
293
- const modelsRaw = context.item.config?.models;
308
+ const config = context.item.config;
309
+ const modelsRaw = config?.models;
294
310
  const models = Array.isArray(modelsRaw)
295
311
  ? modelsRaw
296
312
  : modelsRaw && typeof modelsRaw === "object"
@@ -307,7 +323,7 @@ export function hasChoiceInteraction(context) {
307
323
  // a named model showed the answer eliminator on items where the tool does
308
324
  // nothing. The heuristic remains for configs that name no element.
309
325
  if (type)
310
- return interactionTypes.includes(type);
326
+ return isChoiceElement(type, config?.elements);
311
327
  return Array.isArray(m.choices) && m.choices.length > 0;
312
328
  });
313
329
  }
@@ -8,74 +8,33 @@
8
8
  *
9
9
  * Part of PIE Assessment Toolkit.
10
10
  */
11
- import type { ITTSProvider, TTSConfig } from "@pie-players/pie-tts";
11
+ import type { ITTSProvider } from "@pie-players/pie-tts";
12
+ import type { RuntimeTTSConfig, TTSRuntimeSettings } from "../tts-runtime-config.js";
12
13
  import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
14
+ export type TTSBackend = NonNullable<TTSRuntimeSettings["backend"]>;
13
15
  /**
14
- * TTS backend type
16
+ * The runtime provider config plus the backend selection and instrumentation
17
+ * this tool provider reads.
15
18
  */
16
- export type TTSBackend = "browser" | "polly" | "google" | "server";
17
- /**
18
- * TTS tool provider configuration
19
- */
20
- export interface TTSToolProviderConfig extends Partial<TTSConfig> {
21
- /**
22
- * TTS backend to use
23
- */
19
+ export type TTSToolProviderConfig = RuntimeTTSConfig & {
24
20
  backend: TTSBackend;
21
+ serverProvider?: TTSRuntimeSettings["serverProvider"];
25
22
  /**
26
- * Server API endpoint (for server backends)
27
- * @example '/api/tts/synthesize'
28
- * @example 'https://api.example.com/tts/synthesize'
29
- */
30
- apiEndpoint?: string;
31
- /**
32
- * Provider to use on server ('polly', 'google')
33
- * Only used when backend is 'server', 'polly', or 'google'
34
- */
35
- serverProvider?: "polly" | "google" | "custom";
36
- /**
37
- * Explicit transport mode for server provider payload translation.
38
- */
39
- transportMode?: "pie" | "custom";
40
- /**
41
- * Endpoint style used by the server-backed provider.
42
- */
43
- endpointMode?: "synthesizePath" | "rootPost";
44
- /**
45
- * Endpoint validation strategy when validateEndpoint=true.
46
- */
47
- endpointValidationMode?: "voices" | "endpoint" | "none";
48
- /**
49
- * Include auth header when fetching remote audio/speech-mark assets.
50
- */
51
- includeAuthOnAssetFetch?: boolean;
52
- /**
53
- * Auth token (if required)
54
- * Typically fetched via authFetcher in ToolProviderRegistry
23
+ * Bearer token for a server backend, sent as `Authorization`. A host supplies
24
+ * it through `provider.runtime.authFetcher`.
55
25
  */
56
26
  authToken?: string;
57
- /**
58
- * Organization ID for multi-tenant applications
59
- */
60
- organizationId?: string;
61
- /**
62
- * Default voice to use
63
- */
64
- voice?: string;
65
- /**
66
- * Speech rate (0.25 to 4.0, default 1.0)
67
- */
68
- rate?: number;
69
- /**
70
- * Speech pitch (0 to 2, default 1.0)
71
- * Note: Only browser backend supports pitch
72
- */
73
- pitch?: number;
74
- /**
75
- * Optional telemetry callback for tool/backend instrumentation.
76
- */
77
27
  onTelemetry?: (eventName: string, payload?: Record<string, unknown>) => void | Promise<void>;
78
- }
28
+ };
29
+ /**
30
+ * Construction options. `loadServerProvider` resolves the server adapter's
31
+ * provider class; a server backend fails to initialize without it. The toolkit
32
+ * leaves the adapter to its caller so that a bundler building the toolkit never
33
+ * has to resolve `@pie-players/tts-client-server`.
34
+ */
35
+ export type TTSToolProviderOptions = {
36
+ loadServerProvider?: () => Promise<new () => ITTSProvider>;
37
+ };
79
38
  /**
80
39
  * TTS Tool Provider
81
40
  *
@@ -89,13 +48,14 @@ export interface TTSToolProviderConfig extends Partial<TTSConfig> {
89
48
  * const ttsProvider = await provider.createInstance();
90
49
  * ```
91
50
  *
92
- * @example Server TTS (with auth)
51
+ * @example Server TTS
93
52
  * ```typescript
94
- * const provider = new TTSToolProvider();
53
+ * const provider = new TTSToolProvider('polly', {
54
+ * loadServerProvider: async () => ServerTTSProvider,
55
+ * });
95
56
  * await provider.initialize({
96
57
  * backend: 'polly',
97
- * apiEndpoint: '/api/tts/synthesize',
98
- * authToken: 'bearer-token', // Fetched via authFetcher
58
+ * apiEndpoint: '/api/tts',
99
59
  * });
100
60
  * const ttsProvider = await provider.createInstance();
101
61
  * ```
@@ -108,13 +68,15 @@ export declare class TTSToolProvider implements ToolProviderApi<TTSToolProviderC
108
68
  readonly requiresAuth: boolean;
109
69
  private ttsProvider;
110
70
  private config;
71
+ private readonly loadServerProvider;
111
72
  private emitTelemetry;
112
73
  /**
113
74
  * Create TTS tool provider
114
75
  *
115
76
  * @param backend TTS backend to use (default: 'browser')
77
+ * @param options Loader for the server adapter, required by server backends
116
78
  */
117
- constructor(backend?: TTSBackend);
79
+ constructor(backend?: TTSBackend, options?: TTSToolProviderOptions);
118
80
  /**
119
81
  * Initialize TTS provider
120
82
  *
@@ -9,6 +9,36 @@
9
9
  * Part of PIE Assessment Toolkit.
10
10
  */
11
11
  import { BrowserTTSProvider } from "../../services/tts/browser-provider.js";
12
+ /**
13
+ * Binds a server backend's provider to the config the registry initialized this
14
+ * tool provider with, including what `provider.runtime.authFetcher` returned.
15
+ * The bound config wins over the runtime config the TTS service initializes the
16
+ * provider with; `headers` and `providerOptions` merge key by key, keeping the
17
+ * host's headers and the service's telemetry reporter.
18
+ */
19
+ function bindServerBackendConfig(provider, bound) {
20
+ return {
21
+ providerId: provider.providerId,
22
+ providerName: provider.providerName,
23
+ version: provider.version,
24
+ initialize: (config) => {
25
+ const runtime = config;
26
+ const merged = {
27
+ ...runtime,
28
+ ...bound,
29
+ headers: { ...runtime.headers, ...bound.headers },
30
+ providerOptions: {
31
+ ...runtime.providerOptions,
32
+ ...bound.providerOptions,
33
+ },
34
+ };
35
+ return provider.initialize(merged);
36
+ },
37
+ supportsFeature: (feature) => provider.supportsFeature(feature),
38
+ getCapabilities: () => provider.getCapabilities(),
39
+ destroy: () => provider.destroy(),
40
+ };
41
+ }
12
42
  /**
13
43
  * TTS Tool Provider
14
44
  *
@@ -22,13 +52,14 @@ import { BrowserTTSProvider } from "../../services/tts/browser-provider.js";
22
52
  * const ttsProvider = await provider.createInstance();
23
53
  * ```
24
54
  *
25
- * @example Server TTS (with auth)
55
+ * @example Server TTS
26
56
  * ```typescript
27
- * const provider = new TTSToolProvider();
57
+ * const provider = new TTSToolProvider('polly', {
58
+ * loadServerProvider: async () => ServerTTSProvider,
59
+ * });
28
60
  * await provider.initialize({
29
61
  * backend: 'polly',
30
- * apiEndpoint: '/api/tts/synthesize',
31
- * authToken: 'bearer-token', // Fetched via authFetcher
62
+ * apiEndpoint: '/api/tts',
32
63
  * });
33
64
  * const ttsProvider = await provider.createInstance();
34
65
  * ```
@@ -41,6 +72,7 @@ export class TTSToolProvider {
41
72
  requiresAuth;
42
73
  ttsProvider = null;
43
74
  config = null;
75
+ loadServerProvider;
44
76
  async emitTelemetry(eventName, payload) {
45
77
  try {
46
78
  await this.config?.onTelemetry?.(eventName, payload);
@@ -53,9 +85,11 @@ export class TTSToolProvider {
53
85
  * Create TTS tool provider
54
86
  *
55
87
  * @param backend TTS backend to use (default: 'browser')
88
+ * @param options Loader for the server adapter, required by server backends
56
89
  */
57
- constructor(backend = "browser") {
90
+ constructor(backend = "browser", options = {}) {
58
91
  this.requiresAuth = backend !== "browser";
92
+ this.loadServerProvider = options.loadServerProvider;
59
93
  }
60
94
  /**
61
95
  * Initialize TTS provider
@@ -103,16 +137,19 @@ export class TTSToolProvider {
103
137
  if (!config.apiEndpoint) {
104
138
  throw new Error("[TTSToolProvider] apiEndpoint required for server-based TTS backends");
105
139
  }
106
- // Server backends are optional; load implementation only when requested.
140
+ const loadServerProvider = this.loadServerProvider;
141
+ if (!loadServerProvider) {
142
+ throw new Error("[TTSToolProvider] server-based TTS backends need the loadServerProvider option");
143
+ }
107
144
  const moduleLoadStartedAt = Date.now();
108
145
  await this.emitTelemetry("pie-tool-library-load-start", {
109
146
  toolId: "textToSpeech",
110
147
  operation: "server-provider-module-import",
111
148
  backend: config.serverProvider || config.backend,
112
149
  });
113
- const serverModule = await (async () => {
150
+ const ServerProvider = await (async () => {
114
151
  try {
115
- const loaded = (await import("@pie-players/tts-client-server"));
152
+ const loaded = await loadServerProvider();
116
153
  await this.emitTelemetry("pie-tool-library-load-success", {
117
154
  toolId: "textToSpeech",
118
155
  operation: "server-provider-module-import",
@@ -133,7 +170,8 @@ export class TTSToolProvider {
133
170
  throw error;
134
171
  }
135
172
  })();
136
- this.ttsProvider = new serverModule.ServerTTSProvider();
173
+ const { backend, serverProvider, onTelemetry, ...backendConfig } = config;
174
+ this.ttsProvider = bindServerBackendConfig(new ServerProvider(), backendConfig);
137
175
  console.log(`[TTSToolProvider] Server TTS initialized (provider: ${config.serverProvider || config.backend})`);
138
176
  }
139
177
  /**
@@ -61,18 +61,18 @@ export interface ToolProviderConfig<TConfig = any> {
61
61
  * const registry = new ToolProviderRegistry();
62
62
  *
63
63
  * // Register a provider with auth fetcher
64
- * registry.register('desmos-calculator', {
65
- * provider: new DesmosToolProvider(),
64
+ * registry.register('example-provider', {
65
+ * provider: new ExampleToolProvider(),
66
66
  * config: {},
67
67
  * lazy: true,
68
68
  * authFetcher: async () => {
69
- * const response = await fetch('/api/desmos/token');
69
+ * const response = await fetch('/api/example/token');
70
70
  * return response.json();
71
71
  * },
72
72
  * });
73
73
  *
74
74
  * // Get provider (auto-initializes if lazy)
75
- * const desmosProvider = await registry.getProvider('desmos-calculator');
75
+ * const provider = await registry.getProvider('example-provider');
76
76
  * ```
77
77
  */
78
78
  export declare class ToolProviderRegistry {
@@ -20,18 +20,18 @@
20
20
  * const registry = new ToolProviderRegistry();
21
21
  *
22
22
  * // Register a provider with auth fetcher
23
- * registry.register('desmos-calculator', {
24
- * provider: new DesmosToolProvider(),
23
+ * registry.register('example-provider', {
24
+ * provider: new ExampleToolProvider(),
25
25
  * config: {},
26
26
  * lazy: true,
27
27
  * authFetcher: async () => {
28
- * const response = await fetch('/api/desmos/token');
28
+ * const response = await fetch('/api/example/token');
29
29
  * return response.json();
30
30
  * },
31
31
  * });
32
32
  *
33
33
  * // Get provider (auto-initializes if lazy)
34
- * const desmosProvider = await registry.getProvider('desmos-calculator');
34
+ * const provider = await registry.getProvider('example-provider');
35
35
  * ```
36
36
  */
37
37
  export class ToolProviderRegistry {
@@ -209,13 +209,6 @@ export class ToolProviderRegistry {
209
209
  * @throws Error if provider not registered
210
210
  */
211
211
  async getProvider(providerId, autoInitialize = true) {
212
- console.log("[ToolProviderRegistry] getProvider called", {
213
- providerId,
214
- autoInitialize,
215
- isRegistered: this.providers.has(providerId),
216
- isInitialized: this.initialized.get(providerId),
217
- allProviderIds: Array.from(this.providers.keys()),
218
- });
219
212
  const provider = this.providers.get(providerId);
220
213
  if (!provider) {
221
214
  console.error("[ToolProviderRegistry] Provider not found:", {
@@ -229,11 +222,6 @@ export class ToolProviderRegistry {
229
222
  console.log("[ToolProviderRegistry] Auto-initializing provider:", providerId);
230
223
  await this.initialize(providerId);
231
224
  }
232
- console.log("[ToolProviderRegistry] Returning provider:", {
233
- providerId,
234
- providerName: provider.providerName,
235
- isReady: provider.isReady(),
236
- });
237
225
  return provider;
238
226
  }
239
227
  /**
@@ -6,14 +6,5 @@
6
6
  *
7
7
  * Part of PIE Assessment Toolkit.
8
8
  */
9
- export type { ToolProviderApi, ToolCategory, ToolProviderCapabilities, } from "./ToolProviderApi.js";
10
9
  export { ToolProviderRegistry } from "./ToolProviderRegistry.js";
11
- export type { ToolProviderConfig } from "./ToolProviderRegistry.js";
12
- export { DesmosToolProvider } from "./DesmosToolProvider.js";
13
- export type { DesmosToolProviderConfig } from "./DesmosToolProvider.js";
14
- export { CortexToolProvider } from "./CortexToolProvider.js";
15
- export type { CortexToolProviderConfig } from "./CortexToolProvider.js";
16
- export { GeoGebraToolProvider } from "./GeoGebraToolProvider.js";
17
- export type { GeoGebraToolProviderConfig } from "./GeoGebraToolProvider.js";
18
10
  export { TTSToolProvider } from "./TTSToolProvider.js";
19
- export type { TTSToolProviderConfig, TTSBackend, } from "./TTSToolProvider.js";
@@ -8,8 +8,6 @@
8
8
  */
9
9
  // Registry
10
10
  export { ToolProviderRegistry } from "./ToolProviderRegistry.js";
11
- // Concrete providers
12
- export { DesmosToolProvider } from "./DesmosToolProvider.js";
13
- export { CortexToolProvider } from "./CortexToolProvider.js";
14
- export { GeoGebraToolProvider } from "./GeoGebraToolProvider.js";
11
+ // Concrete providers. The calculator adapters live in the composition layer,
12
+ // `@pie-players/pie-default-tool-loaders`, which owns the engine imports.
15
13
  export { TTSToolProvider } from "./TTSToolProvider.js";