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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -4,40 +4,28 @@
4
4
  * Defines the context types passed to tools for making visibility and behavior decisions.
5
5
  * Each context level provides increasingly specific information about where the tool is being used.
6
6
  */
7
- /**
8
- * Type guard to check if context is at assessment level
9
- */
10
- export function isAssessmentContext(context) {
11
- return context.level === "assessment";
12
- }
13
- /**
14
- * Type guard to check if context is at section level
15
- */
16
- export function isSectionContext(context) {
17
- return context.level === "section";
18
- }
19
7
  /**
20
8
  * Type guard to check if context is at item level
21
9
  */
22
- export function isItemContext(context) {
10
+ function isItemContext(context) {
23
11
  return context.level === "item";
24
12
  }
25
13
  /**
26
14
  * Type guard to check if context is at passage level
27
15
  */
28
- export function isPassageContext(context) {
16
+ function isPassageContext(context) {
29
17
  return context.level === "passage";
30
18
  }
31
19
  /**
32
20
  * Type guard to check if context is at rubric level
33
21
  */
34
- export function isRubricContext(context) {
22
+ function isRubricContext(context) {
35
23
  return context.level === "rubric";
36
24
  }
37
25
  /**
38
26
  * Type guard to check if context is at element level
39
27
  */
40
- export function isElementContext(context) {
28
+ function isElementContext(context) {
41
29
  return context.level === "element";
42
30
  }
43
31
  const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
@@ -53,6 +41,12 @@ function normalizeModels(modelsRaw) {
53
41
  }
54
42
  return [];
55
43
  }
44
+ /** The model in a config's `models` whose `id` is `elementId`, if any. */
45
+ function findElementModel(modelsRaw, elementId) {
46
+ return normalizeModels(modelsRaw).find((candidate) => !!candidate &&
47
+ typeof candidate === "object" &&
48
+ candidate.id === elementId);
49
+ }
56
50
  /**
57
51
  * Push every string a model carries, one level into its arrays of objects.
58
52
  *
@@ -97,8 +91,7 @@ function collectElementsText(elements, push) {
97
91
  *
98
92
  * `transform` decides what the caller gets. {@link extractTextContent} strips
99
93
  * tags, which is right for prose keyword matching and wrong for structural
100
- * matching: a MathML item's only math signal *is* the `<math>` tag, and stripping
101
- * first left `hasMathContent`'s MathML pattern unreachable.
94
+ * matching: a MathML item's only math signal *is* the `<math>` tag.
102
95
  */
103
96
  function extractContent(context, transform) {
104
97
  const textChunks = [];
@@ -115,10 +108,7 @@ function extractContent(context, transform) {
115
108
  push(elementMarkup);
116
109
  // Model data keyed by this element id: in many items the math is in
117
110
  // `model.prompt`/labels rather than in `elements[elementId]`.
118
- const model = normalizeModels(config.models).find((candidate) => !!candidate &&
119
- typeof candidate === "object" &&
120
- candidate.id === context.elementId);
121
- collectModelText(model, push);
111
+ collectModelText(findElementModel(config.models, context.elementId), push);
122
112
  return joined();
123
113
  }
124
114
  if (isItemContext(context)) {
@@ -160,7 +150,7 @@ function extractContent(context, transform) {
160
150
  return "";
161
151
  }
162
152
  /** The plain text a context carries, tags removed. */
163
- export function extractTextContent(context) {
153
+ function extractTextContent(context) {
164
154
  return extractContent(context, stripHtml);
165
155
  }
166
156
  /**
@@ -169,19 +159,14 @@ export function extractTextContent(context) {
169
159
  * For indicators that live in the markup rather than in the prose — `<math>`
170
160
  * above all, whose whole signal is the element name.
171
161
  */
172
- export function extractMarkupContent(context) {
162
+ function extractMarkupContent(context) {
173
163
  return extractContent(context, (value) => value);
174
164
  }
175
- /**
176
- * Helper to check if context contains mathematical content
177
- * (Basic heuristic - can be overridden by tools)
178
- */
179
165
  /**
180
166
  * Chemical element symbols.
181
167
  *
182
168
  * A real set rather than `[A-Z][a-z]?`: that shape matches "It", "In", "He" and
183
- * "A", which is why the science gate used to answer `true` for any prose that
184
- * began a sentence.
169
+ * "A", so any prose beginning a sentence would read as science.
185
170
  */
186
171
  const ELEMENT_SYMBOLS = new Set([
187
172
  "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si",
@@ -233,9 +218,60 @@ function hasChemicalFormula(text) {
233
218
  }
234
219
  return false;
235
220
  }
221
+ /** Numbers joined by slashes: `3/4`, `12 / 4`, `10/10/2026`. */
222
+ const SLASH_RUN = /\d+(?:\s*\/\s*\d+)+/g;
223
+ /** Numbers joined by hyphens spaced on both sides: `15 - 7`, `1809 - 1865`. */
224
+ const SPACED_HYPHEN_RUN = /\d+(?:[.,]\d+)*(?:\s+-\s+\d+(?:[.,]\d+)*)+/g;
225
+ /**
226
+ * Whether the text holds a fraction or a division: exactly one slash between
227
+ * two numbers. A second slash makes the run a date — `10/10/2026`, `2026/10/10`.
228
+ */
229
+ function hasFraction(text) {
230
+ return (text.match(SLASH_RUN) ?? []).some((run) => run.split("/").length === 2);
231
+ }
232
+ /**
233
+ * Whether the text holds a subtraction written with a hyphen.
234
+ *
235
+ * Between numbers a hyphen is mostly a range, a date or an identifier:
236
+ * `1990-2000`, `2025-26`, `2026-10-10`, `555-1234`. On its own it counts as
237
+ * minus only when spaced on both sides, and then only where the numbers fail to
238
+ * ascend, since a range runs low to high: `15 - 7` counts, `1809 - 1865` does
239
+ * not. The trade is that `3 - 5` on its own reads as a range; with an equals
240
+ * sign next to it, it is caught as an equation.
241
+ */
242
+ function hasSpacedSubtraction(text) {
243
+ return (text.match(SPACED_HYPHEN_RUN) ?? []).some((run) => {
244
+ const values = run
245
+ .split(/\s+-\s+/)
246
+ .map((value) => Number(value.replace(/,/g, "")));
247
+ return values.some((value, index) => index > 0 && value <= values[index - 1]);
248
+ });
249
+ }
250
+ const PERCENTAGE = /\d\s*%/;
251
+ /** A percentage taken of a number: `20% of 50`, `15% of $80`, `el 20% de 50`. */
252
+ const PERCENTAGE_OF_NUMBER = /\d\s*%\s+(?:of|de|van)\s+[$€£]?\s?\d/i;
253
+ /** A money amount: `$40`, `€ 12`, `12,50 €`. */
254
+ const MONEY = /[$€£]\s?\d|\d\s?[€£]/;
255
+ /**
256
+ * Whether the text applies a percentage to a quantity.
257
+ *
258
+ * A percentage on its own is a statistic in prose — "40% of voters", "70% of
259
+ * Earth's surface" — so it counts when taken of a number or when the text
260
+ * carries a money amount, as discount, tax and interest problems do. Beside an
261
+ * operator (`25% = 0.25`) it is caught with the operator.
262
+ */
263
+ function hasAppliedPercentage(text) {
264
+ if (PERCENTAGE_OF_NUMBER.test(text))
265
+ return true;
266
+ return PERCENTAGE.test(text) && MONEY.test(text);
267
+ }
268
+ /**
269
+ * Helper to check if context contains mathematical content
270
+ * (Basic heuristic - can be overridden by tools)
271
+ */
236
272
  export function hasMathContent(context) {
237
- // Structural signals live in the markup: stripping tags first is what left the
238
- // MathML pattern unable to match anything at all.
273
+ // Structural signals live in the markup, so they match before tags are
274
+ // stripped.
239
275
  const markup = extractMarkupContent(context);
240
276
  const structuralIndicators = [
241
277
  /<math[>\s]/i, // MathML
@@ -246,20 +282,23 @@ export function hasMathContent(context) {
246
282
  if (structuralIndicators.some((pattern) => pattern.test(markup)))
247
283
  return true;
248
284
  const text = extractTextContent(context);
249
- // No bare-operator pattern. `/[+\-*/=<>≤≥∑∫√π]/` matched any hyphen or slash,
250
- // so "well-known" and "and/or" made every item mathematical and this predicate
251
- // answered `true` for essentially all content — a gate that does not gate. An
252
- // operator counts only with operands around it, or when the character has no
253
- // prose reading at all.
285
+ // No bare-operator pattern: a lone hyphen or slash matches "well-known" and
286
+ // "and/or", which would make nearly every item mathematical. An operator
287
+ // counts only with operands around it, or when the character has no prose
288
+ // reading at all. Hyphens and slashes between numbers also write dates and
289
+ // ranges, and a percentage alone is a statistic, so those have their own
290
+ // predicates below. An operand may carry a percent sign (`25% = 0.25`).
254
291
  const textIndicators = [
255
292
  /[≤≥≠±×÷∑∫√∞π]/, // Symbols with no prose reading
256
- /\d+\s*[+\-*/×÷=]\s*\d+/, // Simple arithmetic
257
- /\d\s*[<>]\s*\d/, // Numeric comparison
258
- /\b\d+\s*\/\s*\d+\b/, // Fractions
259
- /\b\d+(?:\.\d+)?\s*%/, // Percentages
293
+ /\d\s*%?\s*[+*×÷=−]\s*\d/, // Operators with no date or range reading
294
+ /\d\s*[-/]\s*\d[\d.,]*\s*=|=\s*\d[\d.,]*\s*[-/]\s*\d/, // Hyphen or slash in an equation
295
+ /\d\s*%?\s*[<>]\s*\d/, // Numeric comparison
260
296
  /\^\s*\d/, // Exponents
261
297
  ];
262
- return textIndicators.some((pattern) => pattern.test(text));
298
+ return (textIndicators.some((pattern) => pattern.test(text)) ||
299
+ hasFraction(text) ||
300
+ hasSpacedSubtraction(text) ||
301
+ hasAppliedPercentage(text));
263
302
  }
264
303
  const CHOICE_INTERACTION_ELEMENTS = new Set([
265
304
  "multiple-choice",
@@ -294,11 +333,7 @@ export function hasChoiceInteraction(context) {
294
333
  const config = context.item.config;
295
334
  if (!config?.models)
296
335
  return false;
297
- // Find model for this element
298
- const models = Array.isArray(config.models)
299
- ? config.models
300
- : Object.values(config.models);
301
- const model = models.find((m) => m && typeof m === "object" && m.id === context.elementId);
336
+ const model = findElementModel(config.models, context.elementId);
302
337
  if (!model)
303
338
  return false;
304
339
  const type = model.element || "";
@@ -306,25 +341,19 @@ export function hasChoiceInteraction(context) {
306
341
  }
307
342
  if (isItemContext(context)) {
308
343
  const config = context.item.config;
309
- const modelsRaw = config?.models;
310
- const models = Array.isArray(modelsRaw)
311
- ? modelsRaw
312
- : modelsRaw && typeof modelsRaw === "object"
313
- ? Object.values(modelsRaw)
314
- : [];
315
- return models.some((m) => {
316
- if (!m || typeof m !== "object")
344
+ return normalizeModels(config?.models).some((candidate) => {
345
+ if (!candidate || typeof candidate !== "object")
317
346
  return false;
318
- const type = m.element || "";
347
+ const model = candidate;
348
+ const type = model.element || "";
319
349
  // A model that names its element has answered the question, whichever way
320
- // the answer falls. `choices` is carried by interactions that are not
321
- // choice interactions at all — `placement-ordering`, `categorize` and
322
- // `drag-in-the-blank` each hold their draggables there — so reading it on
323
- // a named model showed the answer eliminator on items where the tool does
324
- // nothing. The heuristic remains for configs that name no element.
350
+ // the answer falls: `placement-ordering`, `categorize` and
351
+ // `drag-in-the-blank` each hold their draggables in `choices` without
352
+ // being choice interactions. The `choices` heuristic remains for configs
353
+ // that name no element.
325
354
  if (type)
326
355
  return isChoiceElement(type, config?.elements);
327
- return Array.isArray(m.choices) && m.choices.length > 0;
356
+ return Array.isArray(model.choices) && model.choices.length > 0;
328
357
  });
329
358
  }
330
359
  return false;
@@ -337,15 +366,20 @@ export function hasReadableText(context) {
337
366
  // Must have at least 10 characters of text (arbitrary threshold)
338
367
  return text.trim().length >= 10;
339
368
  }
369
+ /**
370
+ * Whether read-aloud has something to read: readable text, or markup carrying
371
+ * a catalog idref, whose spoken card reads for content with no text.
372
+ */
373
+ export function hasSpokenContent(context) {
374
+ return (hasReadableText(context) ||
375
+ /\bdata-catalog-idref\s*=/.test(extractMarkupContent(context)));
376
+ }
340
377
  /**
341
378
  * Helper to check if context contains science content
342
379
  * (Basic heuristic - can be overridden by tools)
343
380
  */
344
381
  export function hasScienceContent(context) {
345
382
  const text = extractTextContent(context);
346
- // The element-symbol pattern used to be `/\b[A-Z][a-z]?\d*\b/`, which matches
347
- // any one- or two-letter capitalised word: "It", "In", "A", "No". Every item
348
- // beginning a sentence with one read as science.
349
383
  if (hasChemicalFormula(text))
350
384
  return true;
351
385
  if (/[A-Z][a-z]?[\u2080-\u2089]/.test(text))
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where a tool failed, as reported to the toolkit's tool failure policy.
3
+ *
4
+ * `tool-module-load` follows the start-failure policy: the tool degrades unless
5
+ * policy grants it, in which case the failure is fatal. Every other phase keeps a
6
+ * recovery at its report site and is recoverable:
7
+ *
8
+ * - `tool-request-open`: a toolbar threw opening a requested tool; the request
9
+ * reports that no toolbar claimed it.
10
+ * - `tool-request-host-check`: a toolbar threw answering whether it hosts a tool,
11
+ * and is passed by.
12
+ * - `tool-visibility`: a registration's relevance check threw; the tool is
13
+ * withheld unless a grant protects it.
14
+ * - `tool-applicability`: a registration's applicability gate threw; the tool
15
+ * counts as applicable.
16
+ * - `tool-state-load`, `tool-state-save`: a tool could not restore or keep its
17
+ * own learner state, as the annotation toolbar's highlights.
18
+ * - `tool-playback`: a tool could not play, seek or change the rate of speech
19
+ * after speech started; a start failure is reported as `tts-init`.
20
+ */
21
+ export type ToolFailurePhase = "tool-module-load" | "tool-request-open" | "tool-request-host-check" | "tool-visibility" | "tool-applicability" | "tool-state-load" | "tool-state-save" | "tool-playback";
22
+ /** The coordinator surface a failing tool reports through. */
23
+ export interface ToolFailureReporter {
24
+ reportToolFailure?(toolId: string, phase: ToolFailurePhase, error: unknown): void;
25
+ }
26
+ /**
27
+ * Report a tool failure to the coordinator in scope, or log it when there is
28
+ * none, as for a tool mounted outside a toolkit.
29
+ */
30
+ export declare function reportToolFailure(coordinator: ToolFailureReporter | null | undefined, toolId: string, phase: ToolFailurePhase, error: unknown): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Report a tool failure to the coordinator in scope, or log it when there is
3
+ * none, as for a tool mounted outside a toolkit.
4
+ */
5
+ export function reportToolFailure(coordinator, toolId, phase, error) {
6
+ if (typeof coordinator?.reportToolFailure === "function") {
7
+ coordinator.reportToolFailure(toolId, phase, error);
8
+ return;
9
+ }
10
+ console.error(`[${toolId}] ${phase} failed:`, error);
11
+ }
@@ -4,15 +4,15 @@
4
4
  * A registration names an icon rather than shipping markup, so a host swapping the
5
5
  * button chrome gets one consistent set. A name with no entry here renders no icon
6
6
  * at all, and an icon-only button with no icon is a blank square — so a registration
7
- * naming an icon and this map are one change, not two.
7
+ * that names a new icon adds its entry here in the same change.
8
8
  *
9
9
  * Names are generic shapes ("book-open", "beaker"), never capability names. Core
10
10
  * naming a capability is what the composition layer exists to prevent.
11
11
  *
12
- * Exported through `tools/internal` as well: a registration composing a selection
13
- * gateway renders its own buttons and has to draw the same icon the toolbar button
14
- * draws, or the learner sees two unrelated affordances for one tool.
12
+ * `resolveFallbackToolIcon` is exported through `tools/registration` as well: a
13
+ * registration composing a selection gateway renders its own buttons and has to
14
+ * draw the same icon the toolbar button draws, or the learner sees two unrelated
15
+ * affordances for one tool.
15
16
  */
16
- export declare const TOOL_FALLBACK_ICONS: Readonly<Record<string, string>>;
17
17
  /** Icon markup for `name`, or `null` when nothing built in matches. */
18
18
  export declare function resolveFallbackToolIcon(name: string): string | null;
@@ -4,16 +4,17 @@
4
4
  * A registration names an icon rather than shipping markup, so a host swapping the
5
5
  * button chrome gets one consistent set. A name with no entry here renders no icon
6
6
  * at all, and an icon-only button with no icon is a blank square — so a registration
7
- * naming an icon and this map are one change, not two.
7
+ * that names a new icon adds its entry here in the same change.
8
8
  *
9
9
  * Names are generic shapes ("book-open", "beaker"), never capability names. Core
10
10
  * naming a capability is what the composition layer exists to prevent.
11
11
  *
12
- * Exported through `tools/internal` as well: a registration composing a selection
13
- * gateway renders its own buttons and has to draw the same icon the toolbar button
14
- * draws, or the learner sees two unrelated affordances for one tool.
12
+ * `resolveFallbackToolIcon` is exported through `tools/registration` as well: a
13
+ * registration composing a selection gateway renders its own buttons and has to
14
+ * draw the same icon the toolbar button draws, or the learner sees two unrelated
15
+ * affordances for one tool.
15
16
  */
16
- export const TOOL_FALLBACK_ICONS = {
17
+ const TOOL_FALLBACK_ICONS = {
17
18
  calculator: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 2h10a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm0 2v4h10V4H7Zm0 6v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Zm-8 4v2h2v-2H7Zm4 0v2h2v-2h-2Zm4 0v2h2v-2h-2Z" fill="currentColor"/></svg>',
18
19
  "volume-up": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 3.23v2.06A7.002 7.002 0 0 1 19 12a7 7 0 0 1-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77Zm-2 17.75V3L7 8H3v8h4l5 5Zm4.5-9a4.5 4.5 0 0 0-2.5-4.03v8.05A4.5 4.5 0 0 0 16.5 12Z" fill="currentColor"/></svg>',
19
20
  swatch: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.55-.45-1-1-1h-2.5a1.5 1.5 0 0 1 0-3H20a1 1 0 0 0 1-1 8.99 8.99 0 0 0-9-4Zm-5.5 9A1.5 1.5 0 1 1 8 13.5 1.5 1.5 0 0 1 6.5 12Zm3-4A1.5 1.5 0 1 1 11 9.5 1.5 1.5 0 0 1 9.5 8Zm5 0A1.5 1.5 0 1 1 16 9.5 1.5 1.5 0 0 1 14.5 8Z" fill="currentColor"/></svg>',
@@ -1,14 +1,15 @@
1
- export declare const DEFAULT_TOOL_SCOPE_LEVELS: readonly ["assessment", "section", "item", "passage", "rubric"];
2
- export type BuiltinToolScopeLevel = (typeof DEFAULT_TOOL_SCOPE_LEVELS)[number];
3
- export type ToolScopeLevel = BuiltinToolScopeLevel | (string & {});
1
+ declare const TOOL_SCOPE_LEVELS: readonly ["assessment", "section", "item", "passage", "rubric"];
2
+ export type ToolScopeLevel = (typeof TOOL_SCOPE_LEVELS)[number];
4
3
  export interface ParsedToolInstanceId {
5
4
  baseToolId: string;
6
5
  scopeLevel: ToolScopeLevel;
7
6
  scopeId: string;
8
7
  }
9
- export declare function getRegisteredToolScopeLevels(): string[];
10
- export declare function registerToolScopeLevel(scopeLevel: string): void;
11
- export declare function isRegisteredToolScopeLevel(scopeLevel: string): scopeLevel is ToolScopeLevel;
8
+ /**
9
+ * A tool instance id, `<baseToolId>:<scopeLevel>:<scopeId>`, from trimmed parts.
10
+ * Throws on an empty part or an unknown scope level.
11
+ */
12
12
  export declare function createScopedToolId(baseToolId: string, scopeLevel: ToolScopeLevel, scopeId: string): string;
13
+ /** The parts of a tool instance id, or `null` when `id` is not one. */
13
14
  export declare function parseScopedToolId(id: string): ParsedToolInstanceId | null;
14
- export declare function toOverlayToolId(id: string): string;
15
+ export {};
@@ -1,35 +1,30 @@
1
- export const DEFAULT_TOOL_SCOPE_LEVELS = [
1
+ const TOOL_SCOPE_LEVELS = [
2
2
  "assessment",
3
3
  "section",
4
4
  "item",
5
5
  "passage",
6
6
  "rubric",
7
7
  ];
8
- const registeredToolScopeLevels = new Set(DEFAULT_TOOL_SCOPE_LEVELS);
9
- export function getRegisteredToolScopeLevels() {
10
- return Array.from(registeredToolScopeLevels.values());
11
- }
12
- export function registerToolScopeLevel(scopeLevel) {
13
- const normalized = scopeLevel.trim();
14
- if (!normalized) {
15
- throw new Error("Tool scope level must be a non-empty string");
16
- }
17
- registeredToolScopeLevels.add(normalized);
18
- }
19
- export function isRegisteredToolScopeLevel(scopeLevel) {
20
- return registeredToolScopeLevels.has(scopeLevel);
8
+ const toolScopeLevels = new Set(TOOL_SCOPE_LEVELS);
9
+ function isToolScopeLevel(scopeLevel) {
10
+ return toolScopeLevels.has(scopeLevel);
21
11
  }
12
+ /**
13
+ * A tool instance id, `<baseToolId>:<scopeLevel>:<scopeId>`, from trimmed parts.
14
+ * Throws on an empty part or an unknown scope level.
15
+ */
22
16
  export function createScopedToolId(baseToolId, scopeLevel, scopeId) {
23
17
  const normalizedBase = baseToolId.trim();
24
18
  const normalizedScopeId = scopeId.trim();
25
19
  if (!normalizedBase || !normalizedScopeId) {
26
20
  throw new Error("Tool instance ids require non-empty tool and scope ids");
27
21
  }
28
- if (!isRegisteredToolScopeLevel(scopeLevel)) {
29
- throw new Error(`Unknown tool scope level '${scopeLevel}'. Register custom levels with registerToolScopeLevel().`);
22
+ if (!isToolScopeLevel(scopeLevel)) {
23
+ throw new Error(`Unknown tool scope level '${scopeLevel}'.`);
30
24
  }
31
25
  return `${normalizedBase}:${scopeLevel}:${normalizedScopeId}`;
32
26
  }
27
+ /** The parts of a tool instance id, or `null` when `id` is not one. */
33
28
  export function parseScopedToolId(id) {
34
29
  const parts = id.split(":");
35
30
  if (parts.length !== 3)
@@ -37,18 +32,11 @@ export function parseScopedToolId(id) {
37
32
  const [baseToolId, scopeLevelRaw, scopeId] = parts;
38
33
  if (!baseToolId || !scopeId)
39
34
  return null;
40
- if (!isRegisteredToolScopeLevel(scopeLevelRaw)) {
35
+ if (!isToolScopeLevel(scopeLevelRaw))
41
36
  return null;
42
- }
43
37
  return {
44
38
  baseToolId,
45
39
  scopeLevel: scopeLevelRaw,
46
40
  scopeId,
47
41
  };
48
42
  }
49
- export function toOverlayToolId(id) {
50
- const parsed = parseScopedToolId(id);
51
- if (!parsed)
52
- return id;
53
- return createScopedToolId(parsed.baseToolId, parsed.scopeLevel, parsed.scopeId);
54
- }
@@ -1,24 +1,22 @@
1
1
  /**
2
2
  * TTS Tool Provider
3
3
  *
4
- * Unified provider for all TTS backends:
5
- * - Browser Web Speech API (no auth)
6
- * - AWS Polly (requires auth via server proxy)
7
- * - Google Cloud TTS (requires auth via server proxy)
8
- *
9
- * Part of PIE Assessment Toolkit.
4
+ * Unified provider for both TTS backends:
5
+ * - `browser`: the Web Speech API (no auth)
6
+ * - `server`: a TTS server, its service named by the config's `provider`.
7
+ * `polly` and `google` run on the `pie` transport; `custom` runs on the
8
+ * `custom` transport to a host's own service, which takes `lang_id`,
9
+ * `speedRate` and `cache`
10
10
  */
11
11
  import type { ITTSProvider } from "@pie-players/pie-tts";
12
12
  import type { RuntimeTTSConfig, TTSRuntimeSettings } from "../tts-runtime-config.js";
13
- import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
13
+ import type { ToolProviderApi } from "./ToolProviderApi.js";
14
14
  export type TTSBackend = NonNullable<TTSRuntimeSettings["backend"]>;
15
15
  /**
16
- * The runtime provider config plus the backend selection and instrumentation
17
- * this tool provider reads.
16
+ * The runtime provider config plus the credentials and instrumentation this
17
+ * tool provider reads. The backend is fixed at construction.
18
18
  */
19
19
  export type TTSToolProviderConfig = RuntimeTTSConfig & {
20
- backend: TTSBackend;
21
- serverProvider?: TTSRuntimeSettings["serverProvider"];
22
20
  /**
23
21
  * Bearer token for a server backend, sent as `Authorization`. A host supplies
24
22
  * it through `provider.runtime.authFetcher`.
@@ -38,88 +36,52 @@ export type TTSToolProviderOptions = {
38
36
  /**
39
37
  * TTS Tool Provider
40
38
  *
41
- * Wraps TTS providers (Browser, Polly, Google) with the ToolProviderApi interface
39
+ * Wraps the browser or a server TTS provider with the ToolProviderApi interface
42
40
  * for use in the ToolProviderRegistry.
43
41
  *
44
42
  * @example Browser TTS (no auth)
45
43
  * ```typescript
46
44
  * const provider = new TTSToolProvider();
47
- * await provider.initialize({ backend: 'browser' });
45
+ * await provider.initialize({});
48
46
  * const ttsProvider = await provider.createInstance();
49
47
  * ```
50
48
  *
51
49
  * @example Server TTS
52
50
  * ```typescript
53
- * const provider = new TTSToolProvider('polly', {
51
+ * const provider = new TTSToolProvider('server', {
54
52
  * loadServerProvider: async () => ServerTTSProvider,
55
53
  * });
56
54
  * await provider.initialize({
57
- * backend: 'polly',
55
+ * provider: 'polly',
58
56
  * apiEndpoint: '/api/tts',
59
57
  * });
60
58
  * const ttsProvider = await provider.createInstance();
61
59
  * ```
62
60
  */
63
- export declare class TTSToolProvider implements ToolProviderApi<TTSToolProviderConfig, ITTSProvider> {
64
- readonly providerId = "tts-service";
61
+ export declare class TTSToolProvider implements ToolProviderApi<ITTSProvider> {
65
62
  readonly providerName = "Text-to-Speech";
66
63
  readonly category: "tts";
67
64
  readonly version = "1.0";
68
65
  readonly requiresAuth: boolean;
66
+ private readonly backend;
69
67
  private ttsProvider;
70
68
  private config;
71
69
  private readonly loadServerProvider;
72
70
  private emitTelemetry;
73
- /**
74
- * Create TTS tool provider
75
- *
76
- * @param backend TTS backend to use (default: 'browser')
77
- * @param options Loader for the server adapter, required by server backends
78
- */
71
+ /** A server backend needs `options.loadServerProvider`. */
79
72
  constructor(backend?: TTSBackend, options?: TTSToolProviderOptions);
80
73
  /**
81
- * Initialize TTS provider
82
- *
83
- * Sets up the appropriate TTS backend.
84
- *
85
- * @param config Configuration with backend type and credentials
86
- * @throws Error if initialization fails or required config missing
74
+ * Set up the backend's TTS provider. Throws when the backend is unavailable
75
+ * or its required config is missing. Once initialized, a call only warns.
87
76
  */
88
77
  initialize(config: TTSToolProviderConfig): Promise<void>;
89
- /**
90
- * Initialize browser TTS (Web Speech API)
91
- */
92
78
  private _initializeBrowserTTS;
93
- /**
94
- * Initialize server-based TTS (Polly, Google)
95
- */
96
79
  private _initializeServerTTS;
97
80
  /**
98
- * Create a TTS provider instance
99
- *
100
- * Returns the initialized TTS provider.
101
- *
102
- * @param config Optional instance-specific configuration (currently unused)
103
- * @returns TTS provider
104
- * @throws Error if provider not initialized
81
+ * The initialized TTS provider; every call returns the same one and ignores
82
+ * `config`. Throws before `initialize`.
105
83
  */
106
84
  createInstance(config?: Partial<TTSToolProviderConfig>): Promise<ITTSProvider>;
107
- /**
108
- * Get provider capabilities
109
- *
110
- * @returns TTS capabilities based on backend
111
- */
112
- getCapabilities(): ToolProviderCapabilities;
113
- /**
114
- * Check if provider is ready
115
- *
116
- * @returns true if provider is initialized
117
- */
118
85
  isReady(): boolean;
119
- /**
120
- * Clean up provider resources
121
- *
122
- * Destroys the TTS provider and releases resources.
123
- */
124
86
  destroy(): void;
125
87
  }