@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.1

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 (142) hide show
  1. package/README.md +451 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-asenkvx6.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +32 -30
  23. package/dist/policy/core/decision-types.d.ts +51 -34
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +7 -3
  27. package/dist/policy/core/pnp-policy-inputs.js +17 -33
  28. package/dist/policy/core/provenance.d.ts +2 -2
  29. package/dist/policy/core/tool-parameters.d.ts +12 -0
  30. package/dist/policy/core/tool-parameters.js +27 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
  33. package/dist/policy/sources/PnpPolicySource.js +57 -29
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +51 -57
  66. package/dist/services/TTSService.js +323 -284
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +83 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +51 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +39 -24
  83. package/dist/services/tool-context.js +158 -92
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +2 -0
  98. package/dist/services/toolbar-decided-tools.js +2 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +13 -22
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +7 -6
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. package/dist/utils/logger.js +0 -10
@@ -4,28 +4,30 @@
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
+ import { runtimeElementTags } from "@pie-players/pie-players-shared/pie";
8
+ import { parseVersionedTagName } from "@pie-players/pie-players-shared/pie/tag-names";
7
9
  /**
8
10
  * Type guard to check if context is at item level
9
11
  */
10
- export function isItemContext(context) {
12
+ function isItemContext(context) {
11
13
  return context.level === "item";
12
14
  }
13
15
  /**
14
16
  * Type guard to check if context is at passage level
15
17
  */
16
- export function isPassageContext(context) {
18
+ function isPassageContext(context) {
17
19
  return context.level === "passage";
18
20
  }
19
21
  /**
20
22
  * Type guard to check if context is at rubric level
21
23
  */
22
- export function isRubricContext(context) {
24
+ function isRubricContext(context) {
23
25
  return context.level === "rubric";
24
26
  }
25
27
  /**
26
28
  * Type guard to check if context is at element level
27
29
  */
28
- export function isElementContext(context) {
30
+ function isElementContext(context) {
29
31
  return context.level === "element";
30
32
  }
31
33
  const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").trim();
@@ -41,6 +43,12 @@ function normalizeModels(modelsRaw) {
41
43
  }
42
44
  return [];
43
45
  }
46
+ /** The model in a config's `models` whose `id` is `elementId`, if any. */
47
+ function findElementModel(modelsRaw, elementId) {
48
+ return normalizeModels(modelsRaw).find((candidate) => !!candidate &&
49
+ typeof candidate === "object" &&
50
+ candidate.id === elementId);
51
+ }
44
52
  /**
45
53
  * Push every string a model carries, one level into its arrays of objects.
46
54
  *
@@ -85,8 +93,7 @@ function collectElementsText(elements, push) {
85
93
  *
86
94
  * `transform` decides what the caller gets. {@link extractTextContent} strips
87
95
  * tags, which is right for prose keyword matching and wrong for structural
88
- * matching: a MathML item's only math signal *is* the `<math>` tag, and stripping
89
- * first left `hasMathContent`'s MathML pattern unreachable.
96
+ * matching: a MathML item's only math signal *is* the `<math>` tag.
90
97
  */
91
98
  function extractContent(context, transform) {
92
99
  const textChunks = [];
@@ -103,10 +110,7 @@ function extractContent(context, transform) {
103
110
  push(elementMarkup);
104
111
  // Model data keyed by this element id: in many items the math is in
105
112
  // `model.prompt`/labels rather than in `elements[elementId]`.
106
- const model = normalizeModels(config.models).find((candidate) => !!candidate &&
107
- typeof candidate === "object" &&
108
- candidate.id === context.elementId);
109
- collectModelText(model, push);
113
+ collectModelText(findElementModel(config.models, context.elementId), push);
110
114
  return joined();
111
115
  }
112
116
  if (isItemContext(context)) {
@@ -148,7 +152,7 @@ function extractContent(context, transform) {
148
152
  return "";
149
153
  }
150
154
  /** The plain text a context carries, tags removed. */
151
- export function extractTextContent(context) {
155
+ function extractTextContent(context) {
152
156
  return extractContent(context, stripHtml);
153
157
  }
154
158
  /**
@@ -157,19 +161,14 @@ export function extractTextContent(context) {
157
161
  * For indicators that live in the markup rather than in the prose — `<math>`
158
162
  * above all, whose whole signal is the element name.
159
163
  */
160
- export function extractMarkupContent(context) {
164
+ function extractMarkupContent(context) {
161
165
  return extractContent(context, (value) => value);
162
166
  }
163
- /**
164
- * Helper to check if context contains mathematical content
165
- * (Basic heuristic - can be overridden by tools)
166
- */
167
167
  /**
168
168
  * Chemical element symbols.
169
169
  *
170
170
  * A real set rather than `[A-Z][a-z]?`: that shape matches "It", "In", "He" and
171
- * "A", which is why the science gate used to answer `true` for any prose that
172
- * began a sentence.
171
+ * "A", so any prose beginning a sentence would read as science.
173
172
  */
174
173
  const ELEMENT_SYMBOLS = new Set([
175
174
  "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si",
@@ -221,9 +220,60 @@ function hasChemicalFormula(text) {
221
220
  }
222
221
  return false;
223
222
  }
223
+ /** Numbers joined by slashes: `3/4`, `12 / 4`, `10/10/2026`. */
224
+ const SLASH_RUN = /\d+(?:\s*\/\s*\d+)+/g;
225
+ /** Numbers joined by hyphens spaced on both sides: `15 - 7`, `1809 - 1865`. */
226
+ const SPACED_HYPHEN_RUN = /\d+(?:[.,]\d+)*(?:\s+-\s+\d+(?:[.,]\d+)*)+/g;
227
+ /**
228
+ * Whether the text holds a fraction or a division: exactly one slash between
229
+ * two numbers. A second slash makes the run a date — `10/10/2026`, `2026/10/10`.
230
+ */
231
+ function hasFraction(text) {
232
+ return (text.match(SLASH_RUN) ?? []).some((run) => run.split("/").length === 2);
233
+ }
234
+ /**
235
+ * Whether the text holds a subtraction written with a hyphen.
236
+ *
237
+ * Between numbers a hyphen is mostly a range, a date or an identifier:
238
+ * `1990-2000`, `2025-26`, `2026-10-10`, `555-1234`. On its own it counts as
239
+ * minus only when spaced on both sides, and then only where the numbers fail to
240
+ * ascend, since a range runs low to high: `15 - 7` counts, `1809 - 1865` does
241
+ * not. The trade is that `3 - 5` on its own reads as a range; with an equals
242
+ * sign next to it, it is caught as an equation.
243
+ */
244
+ function hasSpacedSubtraction(text) {
245
+ return (text.match(SPACED_HYPHEN_RUN) ?? []).some((run) => {
246
+ const values = run
247
+ .split(/\s+-\s+/)
248
+ .map((value) => Number(value.replace(/,/g, "")));
249
+ return values.some((value, index) => index > 0 && value <= values[index - 1]);
250
+ });
251
+ }
252
+ const PERCENTAGE = /\d\s*%/;
253
+ /** A percentage taken of a number: `20% of 50`, `15% of $80`, `el 20% de 50`. */
254
+ const PERCENTAGE_OF_NUMBER = /\d\s*%\s+(?:of|de|van)\s+[$€£]?\s?\d/i;
255
+ /** A money amount: `$40`, `€ 12`, `12,50 €`. */
256
+ const MONEY = /[$€£]\s?\d|\d\s?[€£]/;
257
+ /**
258
+ * Whether the text applies a percentage to a quantity.
259
+ *
260
+ * A percentage on its own is a statistic in prose — "40% of voters", "70% of
261
+ * Earth's surface" — so it counts when taken of a number or when the text
262
+ * carries a money amount, as discount, tax and interest problems do. Beside an
263
+ * operator (`25% = 0.25`) it is caught with the operator.
264
+ */
265
+ function hasAppliedPercentage(text) {
266
+ if (PERCENTAGE_OF_NUMBER.test(text))
267
+ return true;
268
+ return PERCENTAGE.test(text) && MONEY.test(text);
269
+ }
270
+ /**
271
+ * Helper to check if context contains mathematical content
272
+ * (Basic heuristic - can be overridden by tools)
273
+ */
224
274
  export function hasMathContent(context) {
225
- // Structural signals live in the markup: stripping tags first is what left the
226
- // MathML pattern unable to match anything at all.
275
+ // Structural signals live in the markup, so they match before tags are
276
+ // stripped.
227
277
  const markup = extractMarkupContent(context);
228
278
  const structuralIndicators = [
229
279
  /<math[>\s]/i, // MathML
@@ -234,88 +284,99 @@ export function hasMathContent(context) {
234
284
  if (structuralIndicators.some((pattern) => pattern.test(markup)))
235
285
  return true;
236
286
  const text = extractTextContent(context);
237
- // No bare-operator pattern. `/[+\-*/=<>≤≥∑∫√π]/` matched any hyphen or slash,
238
- // so "well-known" and "and/or" made every item mathematical and this predicate
239
- // answered `true` for essentially all content — a gate that does not gate. An
240
- // operator counts only with operands around it, or when the character has no
241
- // prose reading at all.
287
+ // No bare-operator pattern: a lone hyphen or slash matches "well-known" and
288
+ // "and/or", which would make nearly every item mathematical. An operator
289
+ // counts only with operands around it, or when the character has no prose
290
+ // reading at all. Hyphens and slashes between numbers also write dates and
291
+ // ranges, and a percentage alone is a statistic, so those have their own
292
+ // predicates below. An operand may carry a percent sign (`25% = 0.25`).
242
293
  const textIndicators = [
243
294
  /[≤≥≠±×÷∑∫√∞π]/, // Symbols with no prose reading
244
- /\d+\s*[+\-*/×÷=]\s*\d+/, // Simple arithmetic
245
- /\d\s*[<>]\s*\d/, // Numeric comparison
246
- /\b\d+\s*\/\s*\d+\b/, // Fractions
247
- /\b\d+(?:\.\d+)?\s*%/, // Percentages
295
+ /\d\s*%?\s*[+*×÷=−]\s*\d/, // Operators with no date or range reading
296
+ /\d\s*[-/]\s*\d[\d.,]*\s*=|=\s*\d[\d.,]*\s*[-/]\s*\d/, // Hyphen or slash in an equation
297
+ /\d\s*%?\s*[<>]\s*\d/, // Numeric comparison
248
298
  /\^\s*\d/, // Exponents
249
299
  ];
250
- return textIndicators.some((pattern) => pattern.test(text));
300
+ return (textIndicators.some((pattern) => pattern.test(text)) ||
301
+ hasFraction(text) ||
302
+ hasSpacedSubtraction(text) ||
303
+ hasAppliedPercentage(text));
251
304
  }
252
- const CHOICE_INTERACTION_ELEMENTS = new Set([
253
- "multiple-choice",
254
- "inline-choice",
255
- "select-text",
256
- "ebsr",
257
- ]);
258
- // Content names one interaction several ways: `multiple-choice`, `pie-multiple-choice`,
259
- // `pie-element-multiple-choice`, a runtime `--version-*` tag, or any tag the item's
260
- // `elements` map binds to `@pie-element/multiple-choice`.
261
- const baseElementName = (tag) => tag
262
- .toLowerCase()
263
- .replace(/--version-[0-9a-z-]+$/, "")
264
- .replace(/^pie-(element-)?/, "");
265
- function isChoiceElement(tag, elements) {
266
- if (CHOICE_INTERACTION_ELEMENTS.has(baseElementName(tag)))
267
- return true;
305
+ function packageSpecFor(tag, elements) {
268
306
  if (!elements || typeof elements !== "object")
269
- return false;
307
+ return undefined;
270
308
  const map = elements;
271
- const spec = map[tag] ?? map[tag.replace(/--version-[0-9A-Za-z-]+$/, "")];
272
- if (typeof spec !== "string")
273
- return false;
274
- const packageName = spec.match(/^@pie-element\/([^@]+)/)?.[1];
275
- return !!packageName && CHOICE_INTERACTION_ELEMENTS.has(packageName);
309
+ const spec = map[tag] ?? map[parseVersionedTagName(tag).baseName];
310
+ return typeof spec === "string" && spec ? spec : undefined;
311
+ }
312
+ function declaredSupports(elementClass) {
313
+ let declared;
314
+ try {
315
+ declared = elementClass.pieSupports;
316
+ }
317
+ catch {
318
+ return undefined;
319
+ }
320
+ if (!Array.isArray(declared))
321
+ return undefined;
322
+ return declared.filter((term) => typeof term === "string");
323
+ }
324
+ function readElementSupports(model, elements) {
325
+ if (!model || typeof model !== "object")
326
+ return null;
327
+ const { id, element } = model;
328
+ if (typeof element !== "string" || !element)
329
+ return null;
330
+ const packageSpec = packageSpecFor(element, elements);
331
+ const candidates = runtimeElementTags(element, packageSpec);
332
+ const registry = typeof customElements === "undefined" ? undefined : customElements;
333
+ for (const tag of candidates) {
334
+ const elementClass = registry?.get(tag);
335
+ if (elementClass) {
336
+ return {
337
+ elementId: typeof id === "string" ? id : undefined,
338
+ tag: element,
339
+ packageSpec,
340
+ supports: declaredSupports(elementClass),
341
+ pendingTags: [],
342
+ };
343
+ }
344
+ }
345
+ return {
346
+ elementId: typeof id === "string" ? id : undefined,
347
+ tag: element,
348
+ packageSpec,
349
+ supports: undefined,
350
+ pendingTags: registry ? candidates : [],
351
+ };
276
352
  }
277
353
  /**
278
- * Helper to check if context contains choice-based interactions
354
+ * The supports each element of a context declares: the element's own at element
355
+ * level, every element of the item or passage config at those levels, and none
356
+ * elsewhere. A model that names no element renders nothing and is skipped.
279
357
  */
280
- export function hasChoiceInteraction(context) {
358
+ export function elementSupports(context) {
359
+ let config;
360
+ let models;
281
361
  if (isElementContext(context)) {
282
- const config = context.item.config;
283
- if (!config?.models)
284
- return false;
285
- // Find model for this element
286
- const models = Array.isArray(config.models)
287
- ? config.models
288
- : Object.values(config.models);
289
- const model = models.find((m) => m && typeof m === "object" && m.id === context.elementId);
290
- if (!model)
291
- return false;
292
- const type = model.element || "";
293
- return isChoiceElement(type, config.elements);
362
+ config = context.item.config;
363
+ models = [findElementModel(config?.models, context.elementId)];
294
364
  }
295
- if (isItemContext(context)) {
296
- const config = context.item.config;
297
- const modelsRaw = config?.models;
298
- const models = Array.isArray(modelsRaw)
299
- ? modelsRaw
300
- : modelsRaw && typeof modelsRaw === "object"
301
- ? Object.values(modelsRaw)
302
- : [];
303
- return models.some((m) => {
304
- if (!m || typeof m !== "object")
305
- return false;
306
- const type = m.element || "";
307
- // A model that names its element has answered the question, whichever way
308
- // the answer falls. `choices` is carried by interactions that are not
309
- // choice interactions at all — `placement-ordering`, `categorize` and
310
- // `drag-in-the-blank` each hold their draggables there — so reading it on
311
- // a named model showed the answer eliminator on items where the tool does
312
- // nothing. The heuristic remains for configs that name no element.
313
- if (type)
314
- return isChoiceElement(type, config?.elements);
315
- return Array.isArray(m.choices) && m.choices.length > 0;
316
- });
365
+ else if (isItemContext(context)) {
366
+ config = context.item?.config;
367
+ models = normalizeModels(config?.models);
317
368
  }
318
- return false;
369
+ else if (isPassageContext(context)) {
370
+ config = context.passage?.config;
371
+ models = normalizeModels(config?.models);
372
+ }
373
+ else {
374
+ return [];
375
+ }
376
+ return models.flatMap((model) => {
377
+ const supports = readElementSupports(model, config?.elements);
378
+ return supports ? [supports] : [];
379
+ });
319
380
  }
320
381
  /**
321
382
  * Helper to check if context contains text that can be read aloud
@@ -325,15 +386,20 @@ export function hasReadableText(context) {
325
386
  // Must have at least 10 characters of text (arbitrary threshold)
326
387
  return text.trim().length >= 10;
327
388
  }
389
+ /**
390
+ * Whether read-aloud has something to read: readable text, or markup carrying
391
+ * a catalog idref, whose spoken card reads for content with no text.
392
+ */
393
+ export function hasSpokenContent(context) {
394
+ return (hasReadableText(context) ||
395
+ /\bdata-catalog-idref\s*=/.test(extractMarkupContent(context)));
396
+ }
328
397
  /**
329
398
  * Helper to check if context contains science content
330
399
  * (Basic heuristic - can be overridden by tools)
331
400
  */
332
401
  export function hasScienceContent(context) {
333
402
  const text = extractTextContent(context);
334
- // The element-symbol pattern used to be `/\b[A-Z][a-z]?\d*\b/`, which matches
335
- // any one- or two-letter capitalised word: "It", "In", "A", "No". Every item
336
- // beginning a sentence with one read as science.
337
403
  if (hasChemicalFormula(text))
338
404
  return true;
339
405
  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,7 +4,7 @@
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.
@@ -14,6 +14,5 @@
14
14
  * draw the same icon the toolbar button draws, or the learner sees two unrelated
15
15
  * affordances for one tool.
16
16
  */
17
- export declare const TOOL_FALLBACK_ICONS: Readonly<Record<string, string>>;
18
17
  /** Icon markup for `name`, or `null` when nothing built in matches. */
19
18
  export declare function resolveFallbackToolIcon(name: string): string | null;
@@ -4,7 +4,7 @@
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.
@@ -14,7 +14,7 @@
14
14
  * draw the same icon the toolbar button draws, or the learner sees two unrelated
15
15
  * affordances for one tool.
16
16
  */
17
- export const TOOL_FALLBACK_ICONS = {
17
+ const TOOL_FALLBACK_ICONS = {
18
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>',
19
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>',
20
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,13 +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;
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,9 +32,8 @@ 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,
@@ -7,12 +7,10 @@
7
7
  * `polly` and `google` run on the `pie` transport; `custom` runs on the
8
8
  * `custom` transport to a host's own service, which takes `lang_id`,
9
9
  * `speedRate` and `cache`
10
- *
11
- * Part of PIE Assessment Toolkit.
12
10
  */
13
11
  import type { ITTSProvider } from "@pie-players/pie-tts";
14
12
  import type { RuntimeTTSConfig, TTSRuntimeSettings } from "../tts-runtime-config.js";
15
- import type { ToolProviderApi, ToolProviderCapabilities } from "./ToolProviderApi.js";
13
+ import type { ToolProviderApi } from "./ToolProviderApi.js";
16
14
  export type TTSBackend = NonNullable<TTSRuntimeSettings["backend"]>;
17
15
  /**
18
16
  * The runtime provider config plus the credentials and instrumentation this
@@ -70,55 +68,20 @@ export declare class TTSToolProvider implements ToolProviderApi<ITTSProvider> {
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 Runtime configuration 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
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
- * The backend's capabilities. Its features are the speech provider's own,
109
- * and all false before {@link initialize} creates it.
110
- */
111
- getCapabilities(): ToolProviderCapabilities;
112
- /**
113
- * Check if provider is ready
114
- *
115
- * @returns true if provider is initialized
116
- */
117
85
  isReady(): boolean;
118
- /**
119
- * Clean up provider resources
120
- *
121
- * Destroys the TTS provider and releases resources.
122
- */
123
86
  destroy(): void;
124
87
  }