@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
@@ -55,25 +55,17 @@ export type CatalogType = "spoken" | "sign-language" | "transcript" | "braille"
55
55
  /**
56
56
  * The catalog types PIE names, plus the rule for the ones it does not.
57
57
  *
58
- * The type above stays open on purpose: QTI treats the support vocabulary as
59
- * extensible, and closing it here would reject content PIE has no reason to
60
- * reject and could not usefully validate anyway, since catalogs arrive as
61
- * authored JSON rather than through this type. Keeping it open cost something
62
- * though — the named literals were documentation only, so a card written
63
- * `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
64
- * for, and it failed by being invisible rather than by failing. That is what
65
- * `isKnownCatalogType` and the warnings below are for: the openness stays, the
66
- * silence does not.
58
+ * `CatalogType` stays open because QTI treats the support vocabulary as
59
+ * extensible, and catalogs arrive as authored JSON this type never validates.
60
+ * The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
61
+ * for, so `isKnownCatalogType` and the warnings below report it.
67
62
  */
68
63
  export declare const KNOWN_CATALOG_TYPES: ReadonlySet<string>;
69
64
  export declare function isKnownCatalogType(type: string): boolean;
70
65
  /**
71
- * Which of a card's two content slots it fills.
72
- *
73
- * Not a new field on the card and not a second discriminant: the card already
74
- * says which form it is by carrying `content` or `payload`, and the
75
- * exactly-one-of invariant is what makes that unambiguous. This names the
76
- * distinction so a lookup can ask for one.
66
+ * Which of a card's two content slots it fills. The card says so by carrying
67
+ * `content` or `payload`, and the exactly-one-of invariant makes that
68
+ * unambiguous; this type names the distinction so a lookup can ask for one.
77
69
  */
78
70
  export type CatalogCardForm = "content" | "payload";
79
71
  export declare const catalogCardForm: (card: CatalogCard) => CatalogCardForm;
@@ -93,17 +85,15 @@ export interface CatalogLookupOptions {
93
85
  * Preferred content form, when one catalog type legitimately has both on the
94
86
  * same node.
95
87
  *
96
- * The case this exists for is a `spoken` node carrying both a reading script
97
- * and a recording of it — which is APIP's authoring pattern and what QTI 3's
98
- * migration guidance tells you to keep, the script doubling as the source the
99
- * audio was generated from and as the fallback when it cannot play. Before
100
- * this, both resolution rungs took the first card matching type and language,
101
- * so whichever of the two was written second in the array was unreachable and
102
- * nothing said so.
88
+ * The case is a `spoken` node carrying both a reading script and a recording
89
+ * of it, which is APIP's authoring pattern and what QTI 3's migration
90
+ * guidance keeps: the script is the source the audio was generated from and
91
+ * the fallback when it cannot play. Without a form, a lookup returns the
92
+ * first card matching type and language.
103
93
  *
104
94
  * A preference, not a filter: if the requested form is not present, the other
105
- * one is still returned. Callers that cannot use a form must check what they
106
- * got, exactly as they already must for a card of a type they did not expect.
95
+ * one is still returned. Callers that cannot use a form check what they got,
96
+ * as they do for a card of a type they did not expect.
107
97
  */
108
98
  form?: CatalogCardForm;
109
99
  }
@@ -241,9 +231,11 @@ export declare class AccessibilityCatalogResolver {
241
231
  */
242
232
  hasCatalog(catalogId: string): boolean;
243
233
  /**
244
- * Get alternative content for a catalog identifier
234
+ * Get alternative content for a catalog identifier.
245
235
  *
246
- * Priority: Item-level catalogs take precedence over assessment-level
236
+ * With a `context`, the catalog registered for exactly that owner comes
237
+ * first, then the one compatible scoped catalog (several are ambiguous and
238
+ * resolve to null). Item-level catalogs come next, then assessment-level.
247
239
  */
248
240
  getAlternative(catalogId: string, options: CatalogLookupOptions): ResolvedCatalog | null;
249
241
  private resolveCard;
@@ -264,11 +256,9 @@ export declare class AccessibilityCatalogResolver {
264
256
  * Get all available alternatives for a catalog identifier
265
257
  *
266
258
  * Every card goes through `resolveCard`, the same projection `getAlternative`
267
- * uses, so enumeration cannot describe a card differently from the resolution
268
- * that renders it. It was hand-rolled here once and drifted immediately: the
269
- * `signLanguage` alias was folded in on the resolution path only, so a card
270
- * that arrived under the alias rendered correctly and was still reported as
271
- * carrying no payload by anything asking what alternates exist.
259
+ * uses, so enumeration describes a card exactly as the resolution that
260
+ * renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
261
+ * folded in on the resolution path only.
272
262
  */
273
263
  getAllAlternatives(catalogId: string): ResolvedCatalog[];
274
264
  /**
@@ -4,15 +4,10 @@ import { sanitizeSsmlString } from "./SSMLExtractor.js";
4
4
  /**
5
5
  * The catalog types PIE names, plus the rule for the ones it does not.
6
6
  *
7
- * The type above stays open on purpose: QTI treats the support vocabulary as
8
- * extensible, and closing it here would reject content PIE has no reason to
9
- * reject and could not usefully validate anyway, since catalogs arrive as
10
- * authored JSON rather than through this type. Keeping it open cost something
11
- * though — the named literals were documentation only, so a card written
12
- * `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
13
- * for, and it failed by being invisible rather than by failing. That is what
14
- * `isKnownCatalogType` and the warnings below are for: the openness stays, the
15
- * silence does not.
7
+ * `CatalogType` stays open because QTI treats the support vocabulary as
8
+ * extensible, and catalogs arrive as authored JSON this type never validates.
9
+ * The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
10
+ * for, so `isKnownCatalogType` and the warnings below report it.
16
11
  */
17
12
  export const KNOWN_CATALOG_TYPES = new Set([
18
13
  "spoken",
@@ -336,9 +331,11 @@ export class AccessibilityCatalogResolver {
336
331
  Array.from(this.scopedCatalogs.values()).some((owner) => owner.catalogs.has(catalogId)));
337
332
  }
338
333
  /**
339
- * Get alternative content for a catalog identifier
334
+ * Get alternative content for a catalog identifier.
340
335
  *
341
- * Priority: Item-level catalogs take precedence over assessment-level
336
+ * With a `context`, the catalog registered for exactly that owner comes
337
+ * first, then the one compatible scoped catalog (several are ambiguous and
338
+ * resolve to null). Item-level catalogs come next, then assessment-level.
342
339
  */
343
340
  getAlternative(catalogId, options) {
344
341
  // A typo on this side is as silent as one on a card: the lookup simply finds
@@ -513,10 +510,10 @@ export class AccessibilityCatalogResolver {
513
510
  }
514
511
  return false;
515
512
  }
516
- // The single funnel every registration path runs through — the constructor and
517
- // `addItemCatalogs` by way of `indexCatalogs`, and `registerCatalogs`
518
- // directly — which is why the unknown-type report lives here rather than at
519
- // each entry point.
513
+ // The single funnel every registration path runs through (the constructor and
514
+ // `addItemCatalogs` by way of `indexCatalogs`, `registerCatalogs` and
515
+ // `registerOwner` by way of `insertScopedCatalogs`), so the unknown-type
516
+ // report lives here.
520
517
  sanitizeCatalogs(catalogs) {
521
518
  return catalogs.map((catalog) => ({
522
519
  ...catalog,
@@ -541,11 +538,10 @@ export class AccessibilityCatalogResolver {
541
538
  // Language rungs, most specific first: the requested language, then the
542
539
  // default language, then any.
543
540
  //
544
- // Each requested tag expands into its RFC 4647 lookup sequence, so `es-MX`
545
- // tries `es-mx` and then `es` before falling through to the default. Matching
546
- // was `===`, which made a POSIX `es_ES` card — what the Learnosity transform
547
- // emits — unreachable for an `es-ES` request except through the final
548
- // no-constraint rung, i.e. by accident.
541
+ // Each requested tag expands into its RFC 4647 lookup sequence over
542
+ // normalized tags, so `es-MX` tries `es-mx` and then `es` before falling
543
+ // through to the default, and a POSIX `es_ES` card (what the Learnosity
544
+ // transform emits) matches an `es-ES` request on its own rung.
549
545
  const languageRungs = [];
550
546
  const pushLookupRungs = (tag) => {
551
547
  for (const step of languageTagLookupSequence(tag)) {
@@ -563,18 +559,16 @@ export class AccessibilityCatalogResolver {
563
559
  const candidates = catalog.cards.filter((card) => card.catalog === type && matchesLanguage(card));
564
560
  if (candidates.length === 0)
565
561
  continue;
566
- // Form is preferred inside a language rung and never across them: a
567
- // recording in the requested language beats a script in that language,
568
- // but a script in the requested language beats a recording in another
569
- // one. Getting this backwards would answer a Spanish lookup with English
570
- // audio, which is worse than answering it with Spanish text.
562
+ // Language outranks form: form is preferred inside a language rung and
563
+ // never across them, so a Spanish lookup preferring a recording gets a
564
+ // Spanish recording first, then Spanish text, and never English audio
565
+ // ahead of either.
571
566
  if (form) {
572
567
  const preferred = candidates.find((card) => catalogCardForm(card) === form);
573
568
  if (preferred)
574
569
  return preferred;
575
570
  }
576
- // No preference expressed, or the preferred form is absent: first match,
577
- // which is what every caller got before form preference existed.
571
+ // No preference, or the preferred form is absent: the first match.
578
572
  return candidates[0];
579
573
  }
580
574
  return null;
@@ -583,25 +577,20 @@ export class AccessibilityCatalogResolver {
583
577
  * Get all available alternatives for a catalog identifier
584
578
  *
585
579
  * Every card goes through `resolveCard`, the same projection `getAlternative`
586
- * uses, so enumeration cannot describe a card differently from the resolution
587
- * that renders it. It was hand-rolled here once and drifted immediately: the
588
- * `signLanguage` alias was folded in on the resolution path only, so a card
589
- * that arrived under the alias rendered correctly and was still reported as
590
- * carrying no payload by anything asking what alternates exist.
580
+ * uses, so enumeration describes a card exactly as the resolution that
581
+ * renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
582
+ * folded in on the resolution path only.
591
583
  */
592
584
  getAllAlternatives(catalogId) {
593
585
  const results = [];
594
- // Type, language *and* form: one catalog identifier legitimately carries
595
- // several cards of the same type in different languages, and also a script
596
- // and a recording of the same type in the *same* language. Keying on type
597
- // and language alone dropped the second of those on the floor, so anything
598
- // asking what alternates exist under-reported them.
586
+ // Keyed on type, language and form: one catalog identifier can carry cards
587
+ // of one type in several languages, and a script and a recording of one
588
+ // type in the same language.
599
589
  const claimed = new Set();
600
590
  const add = (card, source) => {
601
591
  // The language part is normalized, so a POSIX `es_ES` card and a BCP-47
602
- // `es-ES` card collapse to one entry here exactly as they now collapse on
603
- // the resolution path. Keying on the raw string would report two
604
- // alternates where resolution can only ever return one.
592
+ // `es-ES` card collapse to one entry, as on the resolution path, which
593
+ // only ever returns one of them.
605
594
  const key = `${card.catalog}|${normalizeLanguageTag(card.language)}|${catalogCardForm(card)}`;
606
595
  if (claimed.has(key))
607
596
  return;
@@ -646,7 +635,6 @@ export class AccessibilityCatalogResolver {
646
635
  getStatistics() {
647
636
  const allTypes = new Set();
648
637
  const allLanguages = new Set();
649
- // Collect from assessment catalogs
650
638
  for (const catalog of this.assessmentCatalogs.values()) {
651
639
  for (const card of catalog.cards) {
652
640
  allTypes.add(card.catalog);
@@ -654,7 +642,6 @@ export class AccessibilityCatalogResolver {
654
642
  allLanguages.add(card.language);
655
643
  }
656
644
  }
657
- // Collect from item catalogs
658
645
  for (const catalog of this.itemCatalogs.values()) {
659
646
  for (const card of catalog.cards) {
660
647
  allTypes.add(card.catalog);
@@ -1,21 +1,22 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
10
+ */
11
+ /**
12
+ * The parts of a global element id. `attemptId` is `""` when the host names no
13
+ * attempt. A tool whose state belongs to an item or a section rather than to
14
+ * one element leaves `elementId`, and for a section `itemId`, as `""`.
15
15
  */
16
16
  export interface ElementIdComponents {
17
17
  assessmentId: string;
18
18
  sectionId: string;
19
+ attemptId: string;
19
20
  itemId: string;
20
21
  elementId: string;
21
22
  }
@@ -24,70 +25,51 @@ export declare class ElementToolStateStore {
24
25
  private listeners;
25
26
  private onStateChange;
26
27
  /**
27
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
28
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`, each part with
29
- * `%` written as `%25` and `:` as `%3A`, so an id containing `:` round-trips
30
- * and cannot collide with another split of the same characters.
28
+ * Generates a globally unique element ID from its parts.
29
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
30
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
31
+ * `:` round-trips and cannot collide with another split of the same characters.
31
32
  *
32
33
  * @example
33
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
34
- * // Returns: "demo-assessment:section-1:q1:mc1"
34
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
35
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
36
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
35
37
  */
36
- getGlobalElementId(assessmentId: string, sectionId: string, itemId: string, elementId: string): string;
38
+ getGlobalElementId(parts: ElementIdComponents): string;
37
39
  /**
38
40
  * Parses a global element ID into its component parts.
39
41
  *
40
42
  * @example
41
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
42
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
43
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
44
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
45
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
43
46
  */
44
47
  parseGlobalElementId(globalElementId: string): ElementIdComponents | null;
45
48
  /**
46
49
  * Sets tool state for a specific element.
47
50
  * Notifies all subscribers and triggers persistence callback.
48
- *
49
- * @param globalElementId Composite key identifying the element
50
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
51
- * @param state Tool-specific state object
52
51
  */
53
52
  setState(globalElementId: string, toolId: string, state: unknown): void;
54
- /**
55
- * Gets tool state for a specific element.
56
- *
57
- * @param globalElementId Composite key identifying the element
58
- * @param toolId Tool identifier
59
- * @returns Tool state or undefined if not found
60
- */
53
+ /** One tool's state for an element, or `undefined` when none is set. */
61
54
  getState(globalElementId: string, toolId: string): unknown;
62
- /**
63
- * Gets all tool states for a specific element.
64
- *
65
- * @param globalElementId Composite key identifying the element
66
- * @returns Object with tool states keyed by toolId
67
- */
55
+ /** Every tool's state for an element, keyed by toolId. */
68
56
  getElementState(globalElementId: string): Record<string, unknown>;
69
57
  /**
70
- * Gets all tool states for all elements.
71
- * Used for persistence and debugging.
72
- *
73
- * @returns Nested object: { globalElementId: { toolId: state } }
58
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
59
+ * debugging.
74
60
  */
75
61
  getAllState(): Record<string, Record<string, unknown>>;
76
62
  /**
77
63
  * Subscribes to state changes.
78
64
  * Callback is invoked whenever any element's tool state changes, including
79
- * a `loadState` restore, with a copy of the state that it may keep.
80
- *
81
- * @param callback Function to call on state changes
82
- * @returns Unsubscribe function
65
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
66
+ * the unsubscribe function.
83
67
  */
84
68
  subscribe(callback: (state: Map<string, Map<string, unknown>>) => void): () => void;
85
69
  private _notifyListeners;
86
70
  /**
87
71
  * Sets a callback to be invoked on state changes for persistence integration.
88
72
  * Used by demo/app layer to persist tool state to localStorage or server.
89
- *
90
- * @param callback Function to call with serialized state on changes
91
73
  */
92
74
  setOnStateChange(callback: (state: Record<string, Record<string, unknown>>) => void): void;
93
75
  private _notifyStateChange;
@@ -95,29 +77,18 @@ export declare class ElementToolStateStore {
95
77
  * Loads tool state from serialized format.
96
78
  * Used to restore state from localStorage or server. Subscribers are
97
79
  * notified; the persistence callback is not, so a restore never writes back.
98
- *
99
- * @param state Serialized state object
100
80
  */
101
81
  loadState(state: Record<string, Record<string, unknown>>): void;
102
- /**
103
- * Clears all tool state for a specific element.
104
- *
105
- * @param globalElementId Composite key identifying the element
106
- */
82
+ /** Clears all tool state for a specific element. */
107
83
  clearElement(globalElementId: string): void;
108
84
  /**
109
85
  * Clears state for a specific tool across all elements.
110
86
  * Useful when disabling a tool globally.
111
- *
112
- * @param toolId Tool identifier
113
87
  */
114
88
  clearTool(toolId: string): void;
115
89
  /**
116
- * Clears all tool state for a specific section.
90
+ * Clears all tool state for a specific section, across its attempts.
117
91
  * Useful when unmounting/disposing a section.
118
- *
119
- * @param assessmentId Assessment identifier
120
- * @param sectionId Section identifier
121
92
  */
122
93
  clearSection(assessmentId: string, sectionId: string): void;
123
94
  /**
@@ -1,18 +1,20 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
15
10
  */
11
+ const ID_PARTS = [
12
+ "assessmentId",
13
+ "sectionId",
14
+ "attemptId",
15
+ "itemId",
16
+ "elementId",
17
+ ];
16
18
  function escapeIdPart(part) {
17
19
  return part.replace(/%/g, "%25").replace(/:/g, "%3A");
18
20
  }
@@ -24,41 +26,37 @@ export class ElementToolStateStore {
24
26
  listeners = new Set();
25
27
  onStateChange = null;
26
28
  /**
27
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
28
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`, each part with
29
- * `%` written as `%25` and `:` as `%3A`, so an id containing `:` round-trips
30
- * and cannot collide with another split of the same characters.
29
+ * Generates a globally unique element ID from its parts.
30
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
31
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
32
+ * `:` round-trips and cannot collide with another split of the same characters.
31
33
  *
32
34
  * @example
33
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
34
- * // Returns: "demo-assessment:section-1:q1:mc1"
35
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
36
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
37
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
35
38
  */
36
- getGlobalElementId(assessmentId, sectionId, itemId, elementId) {
37
- return [assessmentId, sectionId, itemId, elementId]
38
- .map(escapeIdPart)
39
- .join(":");
39
+ getGlobalElementId(parts) {
40
+ return ID_PARTS.map((name) => escapeIdPart(parts[name])).join(":");
40
41
  }
41
42
  /**
42
43
  * Parses a global element ID into its component parts.
43
44
  *
44
45
  * @example
45
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
46
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
46
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
47
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
48
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
47
49
  */
48
50
  parseGlobalElementId(globalElementId) {
49
51
  const parts = globalElementId.split(":");
50
- if (parts.length !== 4)
52
+ if (parts.length !== ID_PARTS.length)
51
53
  return null;
52
- const [assessmentId, sectionId, itemId, elementId] = parts.map(unescapeIdPart);
53
- return { assessmentId, sectionId, itemId, elementId };
54
+ const [assessmentId, sectionId, attemptId, itemId, elementId] = parts.map(unescapeIdPart);
55
+ return { assessmentId, sectionId, attemptId, itemId, elementId };
54
56
  }
55
57
  /**
56
58
  * Sets tool state for a specific element.
57
59
  * Notifies all subscribers and triggers persistence callback.
58
- *
59
- * @param globalElementId Composite key identifying the element
60
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
61
- * @param state Tool-specific state object
62
60
  */
63
61
  setState(globalElementId, toolId, state) {
64
62
  let elementState = this.elementStates.get(globalElementId);
@@ -70,22 +68,11 @@ export class ElementToolStateStore {
70
68
  this._notifyListeners();
71
69
  this._notifyStateChange();
72
70
  }
73
- /**
74
- * Gets tool state for a specific element.
75
- *
76
- * @param globalElementId Composite key identifying the element
77
- * @param toolId Tool identifier
78
- * @returns Tool state or undefined if not found
79
- */
71
+ /** One tool's state for an element, or `undefined` when none is set. */
80
72
  getState(globalElementId, toolId) {
81
73
  return this.elementStates.get(globalElementId)?.get(toolId);
82
74
  }
83
- /**
84
- * Gets all tool states for a specific element.
85
- *
86
- * @param globalElementId Composite key identifying the element
87
- * @returns Object with tool states keyed by toolId
88
- */
75
+ /** Every tool's state for an element, keyed by toolId. */
89
76
  getElementState(globalElementId) {
90
77
  const elementState = this.elementStates.get(globalElementId);
91
78
  if (!elementState)
@@ -93,10 +80,8 @@ export class ElementToolStateStore {
93
80
  return Object.fromEntries(elementState.entries());
94
81
  }
95
82
  /**
96
- * Gets all tool states for all elements.
97
- * Used for persistence and debugging.
98
- *
99
- * @returns Nested object: { globalElementId: { toolId: state } }
83
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
84
+ * debugging.
100
85
  */
101
86
  getAllState() {
102
87
  const result = {};
@@ -108,10 +93,8 @@ export class ElementToolStateStore {
108
93
  /**
109
94
  * Subscribes to state changes.
110
95
  * Callback is invoked whenever any element's tool state changes, including
111
- * a `loadState` restore, with a copy of the state that it may keep.
112
- *
113
- * @param callback Function to call on state changes
114
- * @returns Unsubscribe function
96
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
97
+ * the unsubscribe function.
115
98
  */
116
99
  subscribe(callback) {
117
100
  this.listeners.add(callback);
@@ -136,8 +119,6 @@ export class ElementToolStateStore {
136
119
  /**
137
120
  * Sets a callback to be invoked on state changes for persistence integration.
138
121
  * Used by demo/app layer to persist tool state to localStorage or server.
139
- *
140
- * @param callback Function to call with serialized state on changes
141
122
  */
142
123
  setOnStateChange(callback) {
143
124
  this.onStateChange = callback;
@@ -156,8 +137,6 @@ export class ElementToolStateStore {
156
137
  * Loads tool state from serialized format.
157
138
  * Used to restore state from localStorage or server. Subscribers are
158
139
  * notified; the persistence callback is not, so a restore never writes back.
159
- *
160
- * @param state Serialized state object
161
140
  */
162
141
  loadState(state) {
163
142
  this.elementStates.clear();
@@ -167,11 +146,7 @@ export class ElementToolStateStore {
167
146
  }
168
147
  this._notifyListeners();
169
148
  }
170
- /**
171
- * Clears all tool state for a specific element.
172
- *
173
- * @param globalElementId Composite key identifying the element
174
- */
149
+ /** Clears all tool state for a specific element. */
175
150
  clearElement(globalElementId) {
176
151
  this.elementStates.delete(globalElementId);
177
152
  this._notifyListeners();
@@ -180,8 +155,6 @@ export class ElementToolStateStore {
180
155
  /**
181
156
  * Clears state for a specific tool across all elements.
182
157
  * Useful when disabling a tool globally.
183
- *
184
- * @param toolId Tool identifier
185
158
  */
186
159
  clearTool(toolId) {
187
160
  for (const elementState of this.elementStates.values()) {
@@ -191,11 +164,8 @@ export class ElementToolStateStore {
191
164
  this._notifyStateChange();
192
165
  }
193
166
  /**
194
- * Clears all tool state for a specific section.
167
+ * Clears all tool state for a specific section, across its attempts.
195
168
  * Useful when unmounting/disposing a section.
196
- *
197
- * @param assessmentId Assessment identifier
198
- * @param sectionId Section identifier
199
169
  */
200
170
  clearSection(assessmentId, sectionId) {
201
171
  const prefix = `${escapeIdPart(assessmentId)}:${escapeIdPart(sectionId)}:`;
@@ -11,8 +11,6 @@
11
11
  * - Configurable colors and styles
12
12
  * - Annotation persistence via RangeSerializer
13
13
  *
14
- * Part of PIE Assessment Toolkit.
15
- *
16
14
  * Browser Support:
17
15
  * - Chrome/Edge 105+
18
16
  * - Safari 17.2+
@@ -63,6 +61,11 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
63
61
  private rangeSerializer;
64
62
  private themeObserver;
65
63
  constructor();
64
+ /**
65
+ * Re-adapt the TTS colors when the page's theme moves: on the document
66
+ * element, on a `<pie-theme>` host, and when a `<pie-theme>` mounts, which
67
+ * themes its target as it connects.
68
+ */
66
69
  private setupThemeObservation;
67
70
  private parseColor;
68
71
  private relativeLuminance;
@@ -100,12 +103,10 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
100
103
  * lives in a font-driven pseudo-element a CSS range cannot paint) and
101
104
  * whole-expression fallbacks.
102
105
  *
103
- * The supplied element is painted exactly; we deliberately do NOT walk up to
104
- * a containing `<math>` / `<mjx-container>`. That escalation is what made
105
- * MathJax-rendered math highlight as a full block while native MathML tracked
106
- * per-token: a resolved single token must paint as a token, and only a
107
- * genuine expression fallback (where the element already is the container)
108
- * should paint the whole expression.
106
+ * The supplied element is painted exactly, with no walk up to a containing
107
+ * `<math>` / `<mjx-container>`: a resolved token paints as a token, and only
108
+ * an expression fallback, where the element already is the container, paints
109
+ * the whole expression.
109
110
  */
110
111
  highlightTTSWordElement(element: Element): void;
111
112
  /**
@@ -196,23 +197,10 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
196
197
  color: HighlightColor;
197
198
  timestamp?: number;
198
199
  }>, root?: Element): number;
199
- /**
200
- * Highlight a range as the given type: the TTS types paint as
201
- * {@link highlightTTSWord} and {@link highlightTTSSentence} do.
202
- */
203
- highlightRange(range: Range, type: HighlightType, color?: HighlightColor): void;
204
- /**
205
- * Clear highlights of a specific type (interface method)
206
- */
207
- clearHighlights(type: HighlightType): void;
208
200
  /**
209
201
  * Clear all highlights (interface method)
210
202
  */
211
203
  clearAll(): void;
212
- /**
213
- * Check if CSS Highlight API is supported (interface method)
214
- */
215
- isSupported(): boolean;
216
204
  /**
217
205
  * Update TTS highlight style dynamically
218
206
  *