@pie-players/pie-assessment-toolkit 0.3.75 → 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 (142) hide show
  1. package/README.md +449 -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-9gqhfxtn.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 +30 -30
  23. package/dist/policy/core/decision-types.d.ts +44 -32
  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 +5 -2
  27. package/dist/policy/core/pnp-policy-inputs.js +12 -32
  28. package/dist/policy/core/provenance.d.ts +1 -1
  29. package/dist/policy/core/tool-parameters.d.ts +11 -0
  30. package/dist/policy/core/tool-parameters.js +15 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
  33. package/dist/policy/sources/PnpPolicySource.js +30 -28
  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 +49 -56
  66. package/dist/services/TTSService.js +308 -283
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +82 -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 +47 -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 +7 -23
  83. package/dist/services/tool-context.js +99 -53
  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 +1 -0
  98. package/dist/services/toolbar-decided-tools.js +1 -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 +4 -20
  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 +6 -5
  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
@@ -25,7 +25,6 @@ import type { ToolkitCoordinator } from "../services/ToolkitCoordinator.js";
25
25
  import type { SectionControllerEvent, SectionControllerHandle, SectionControllerSessionState } from "../services/section-controller-types.js";
26
26
  import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
27
27
  import type { RuntimeRegistrationDetail } from "./registration-events.js";
28
- import { RuntimeRegistry } from "./RuntimeRegistry.js";
29
28
  /**
30
29
  * Structural view of the resolved section controller, widened with the
31
30
  * extra methods the toolkit's runtime CE calls directly. Optional so
@@ -273,7 +272,12 @@ export declare class SectionControllerBinding {
273
272
  requestMediaPauseForCompetingAudio(): boolean;
274
273
  persist(): Promise<void>;
275
274
  hydrate(): Promise<void>;
276
- getRegistry(): RuntimeRegistry;
275
+ /**
276
+ * Releases the binding and disposes the controller of every section it
277
+ * bound, so a coordinator the host passes or an outer toolkit lends keeps
278
+ * none of this toolkit's sections, and a later mount of one restores it from
279
+ * its session. The coordinator itself stays with its owner.
280
+ */
277
281
  dispose(): Promise<void>;
278
282
  }
279
283
  export {};
@@ -43,6 +43,11 @@ function readRenderableIds(model) {
43
43
  }
44
44
  return ids;
45
45
  }
46
+ /** Whether `ids` holds the shell's runtime or canonical id. */
47
+ function rendersShell(ids, shell) {
48
+ return (ids.has(shell.itemId) ||
49
+ (!!shell.canonicalItemId && ids.has(shell.canonicalItemId)));
50
+ }
46
51
  export class SectionControllerBinding {
47
52
  registry = new RuntimeRegistry();
48
53
  controller = null;
@@ -213,10 +218,7 @@ export class SectionControllerBinding {
213
218
  */
214
219
  isSectionRenderable(args) {
215
220
  const ids = this.sectionRenderableIds;
216
- if (!ids)
217
- return true;
218
- return (ids.has(args.itemId) ||
219
- (!!args.canonicalItemId && ids.has(args.canonicalItemId)));
221
+ return !ids || rendersShell(ids, args);
220
222
  }
221
223
  getSectionKey(sectionId, attemptId) {
222
224
  return `${sectionId}\u0000${attemptId ?? ""}`;
@@ -236,10 +238,8 @@ export class SectionControllerBinding {
236
238
  for (const [key, section] of sections) {
237
239
  if (key === current || !section.renderableIds)
238
240
  continue;
239
- if (!section.renderableIds.has(args.itemId) &&
240
- !(args.canonicalItemId && section.renderableIds.has(args.canonicalItemId))) {
241
+ if (!rendersShell(section.renderableIds, args))
241
242
  continue;
242
- }
243
243
  const owner = this.coordinator?.getSectionController({
244
244
  sectionId: section.sectionId,
245
245
  attemptId: section.attemptId,
@@ -427,18 +427,24 @@ export class SectionControllerBinding {
427
427
  async hydrate() {
428
428
  await this.controller?.hydrate?.();
429
429
  }
430
- getRegistry() {
431
- return this.registry;
432
- }
430
+ /**
431
+ * Releases the binding and disposes the controller of every section it
432
+ * bound, so a coordinator the host passes or an outer toolkit lends keeps
433
+ * none of this toolkit's sections, and a later mount of one restores it from
434
+ * its session. The coordinator itself stays with its owner.
435
+ */
433
436
  async dispose() {
434
437
  this.activeInitToken += 1;
435
438
  this.unsubscribeController?.();
436
439
  this.unsubscribeController = null;
437
- if (this.coordinator && this.sectionId) {
438
- await this.coordinator.disposeSectionController({
439
- sectionId: this.sectionId,
440
- attemptId: this.attemptId,
441
- });
440
+ const coordinator = this.coordinator;
441
+ if (coordinator) {
442
+ for (const section of this.boundSections.values()) {
443
+ await coordinator.disposeSectionController({
444
+ sectionId: section.sectionId,
445
+ attemptId: section.attemptId,
446
+ });
447
+ }
442
448
  }
443
449
  this.registry.clear();
444
450
  this.loadedRenderableKeys.clear();
@@ -28,6 +28,5 @@ export interface SubscriberFanoutHandle {
28
28
  emit(outputs: readonly SectionEngineOutput[]): void;
29
29
  subscribe(listener: EngineOutputListener): () => void;
30
30
  dispose(): void;
31
- getListenerCount(): number;
32
31
  }
33
32
  export declare function createSubscriberFanout(): SubscriberFanoutHandle;
@@ -51,8 +51,5 @@ export function createSubscriberFanout() {
51
51
  function dispose() {
52
52
  listeners.clear();
53
53
  }
54
- function getListenerCount() {
55
- return listeners.size;
56
- }
57
- return { emit, subscribe, dispose, getListenerCount };
54
+ return { emit, subscribe, dispose };
58
55
  }
@@ -44,9 +44,9 @@ export declare const DEFAULT_FRAME_DEADLINE_MS = 100;
44
44
  * Timing primitives, injectable so tests can drive the race deterministically.
45
45
  *
46
46
  * An omitted primitive is resolved from `globalThis` at `schedule()` time
47
- * rather than captured at construction: the pre-fix code also checked per
48
- * emit, and a host can install a `requestAnimationFrame` shim after the
49
- * toolkit is constructed. An explicit `null` opts the primitive out.
47
+ * rather than captured at construction, because a host can install a
48
+ * `requestAnimationFrame` shim after the toolkit is constructed. An explicit
49
+ * `null` opts the primitive out.
50
50
  */
51
51
  export interface CompositionEmitSchedulerTiming {
52
52
  requestFrame?: ((callback: () => void) => number) | null;
@@ -77,7 +77,7 @@ export function createCompositionEmitScheduler(timing = {}) {
77
77
  *
78
78
  * The latch is released before the flush callback runs, so a re-entrant
79
79
  * `schedule()` from inside the flush arms a fresh cycle instead of being
80
- * swallowed. The pre-fix code cleared its latch first for the same reason.
80
+ * swallowed.
81
81
  */
82
82
  function resolve(firedFrom) {
83
83
  if (!pending)
@@ -1,7 +1,3 @@
1
- /** Marks the part of a shell scope that holds the content tools read and annotate. */
2
- export declare const CONTENT_REGION_SELECTOR = "[data-region='content']";
3
- /** Marks the host of a shell scope (`<pie-item-scope>`, `<pie-passage-shell>`). */
4
- export declare const SHELL_SCOPE_HOST_SELECTOR = "[data-pie-shell-root]";
5
1
  /**
6
2
  * The content region of `scope` that tools read and annotate.
7
3
  *
@@ -1,8 +1,8 @@
1
1
  import { composedClosest, flatQuerySelector, } from "../services/tts/flat-tree.js";
2
2
  /** Marks the part of a shell scope that holds the content tools read and annotate. */
3
- export const CONTENT_REGION_SELECTOR = "[data-region='content']";
3
+ const CONTENT_REGION_SELECTOR = "[data-region='content']";
4
4
  /** Marks the host of a shell scope (`<pie-item-scope>`, `<pie-passage-shell>`). */
5
- export const SHELL_SCOPE_HOST_SELECTOR = "[data-pie-shell-root]";
5
+ const SHELL_SCOPE_HOST_SELECTOR = "[data-pie-shell-root]";
6
6
  /**
7
7
  * The content region of `scope` that tools read and annotate.
8
8
  *
@@ -45,7 +45,5 @@ export declare class SectionEngineCore {
45
45
  * construction time if you need every output from `idle` onward.
46
46
  */
47
47
  subscribe(listener: SectionEngineCoreListener): () => void;
48
- /** Test-only: detach every listener. */
49
- disposeListeners(): void;
50
48
  private fanOut;
51
49
  }
@@ -61,10 +61,6 @@ export class SectionEngineCore {
61
61
  this.listeners.delete(listener);
62
62
  };
63
63
  }
64
- /** Test-only: detach every listener. */
65
- disposeListeners() {
66
- this.listeners.clear();
67
- }
68
64
  fanOut(outputs) {
69
65
  const snapshot = Array.from(this.listeners);
70
66
  const frozen = Object.freeze(outputs.slice());
@@ -17,11 +17,6 @@ export interface CohortKey {
17
17
  /** Attempt identifier; empty string if the host did not provide one. */
18
18
  attemptId: string;
19
19
  }
20
- /**
21
- * Stable string form of the cohort tuple used as a key for the
22
- * `loadingCompleteEmittedForCohort` latch and equality checks.
23
- */
24
- export declare function cohortKey(cohort: CohortKey | null): string;
25
20
  /**
26
21
  * `true` iff the two cohorts have the same `(sectionId, attemptId)` pair.
27
22
  * Treats both inputs as identifying the cohort by value, never by
@@ -11,15 +11,6 @@
11
11
  * layout-CE prop shape) but the engine carries it as the empty string
12
12
  * when absent so the cohort key is total.
13
13
  */
14
- /**
15
- * Stable string form of the cohort tuple used as a key for the
16
- * `loadingCompleteEmittedForCohort` latch and equality checks.
17
- */
18
- export function cohortKey(cohort) {
19
- if (!cohort)
20
- return "";
21
- return `${cohort.sectionId}|${cohort.attemptId}`;
22
- }
23
14
  /**
24
15
  * `true` iff the two cohorts have the same `(sectionId, attemptId)` pair.
25
16
  * Treats both inputs as identifying the cohort by value, never by
@@ -26,8 +26,9 @@ export type EngineOutputStageChange = {
26
26
  };
27
27
  /**
28
28
  * Canonical `pie-loading-complete`. One-shot per cohort, emitted when the
29
- * readiness signals satisfy `allLoadingComplete`, so every one of `itemCount`
30
- * items has loaded.
29
+ * readiness signals satisfy `allLoadingComplete`: section ready plus the host's
30
+ * loading signal, which the section player kernel raises once the element
31
+ * pre-warm resolves. `itemCount` is the composition's item count.
31
32
  */
32
33
  export type EngineOutputLoadingComplete = {
33
34
  kind: "loading-complete";
@@ -111,8 +111,9 @@ function applyReadinessUpdate(state, args) {
111
111
  emitStageChange(outputs, phase, state.cohort);
112
112
  }
113
113
  // Canonical `loading-complete` emission. One-shot per cohort, gated
114
- // on the readiness signals indicating every item has finished
115
- // loading.
114
+ // on `allLoadingComplete`: section ready plus the host's loading signal,
115
+ // which the section player kernel raises once the element pre-warm
116
+ // resolves.
116
117
  if (detail.allLoadingComplete && state.cohort && !loadingCompleteEmitted) {
117
118
  loadingCompleteEmitted = true;
118
119
  outputs.push({
@@ -71,16 +71,14 @@ export interface InternalFormativeActionDetail {
71
71
  /**
72
72
  * A Media Time Source becoming available or going away.
73
73
  *
74
- * The one seam through which a timed-media section reaches media, and
75
- * deliberately the *only* one: the stimulus card dispatches this with a native
76
- * `<video>` adapter, and a host wrapping a third-party player dispatches the same
77
- * event with its own port. One code path, two producers — which is what keeps
78
- * "a host can supply its own media element without shipping a PIE element" true
79
- * rather than aspirational.
74
+ * The only seam through which a timed-media section reaches media: the stimulus
75
+ * card dispatches it with a native `<video>` adapter, and a host wrapping a
76
+ * third-party player dispatches it with its own port. One code path with two
77
+ * producers is what lets a host supply its own media element without shipping a
78
+ * PIE element.
80
79
  *
81
- * `source` carries a live object, not serializable data. That is fine and
82
- * intended: this event never crosses a realm, exactly like the element reference
83
- * on `pie-register`.
80
+ * `source` is a live object. The event never crosses a realm, like the element
81
+ * reference on `pie-register`.
84
82
  */
85
83
  export interface InternalMediaTimeSourceDetail {
86
84
  /** The renderable that owns the media, for matching against `stimulusRef`. */
@@ -93,11 +91,10 @@ export interface InternalMediaTimeSourceDetail {
93
91
  * wiring its own player, and omitting the field reads as `"host"` because a
94
92
  * caller constructing this event by hand is one.
95
93
  *
96
- * Load-bearing for precedence: the card re-runs its discovery whenever its
97
- * content changes, so without this a host that supplied a third-party port would
98
- * have it silently replaced by the native element mid-session — and the
99
- * capabilities would flip back with it, which is exactly the "appears to enforce"
100
- * failure this contract exists to prevent.
94
+ * Precedence depends on it: the card re-runs its discovery whenever its content
95
+ * changes, and without the field the native element would replace a host's
96
+ * third-party port mid-session, flipping the capabilities back with it, so a
97
+ * policy would read as enforced while it is not.
101
98
  */
102
99
  origin?: "native-adapter" | "host";
103
100
  }
@@ -2,6 +2,6 @@
2
2
  * Stable runtime-instance id generator.
3
3
  *
4
4
  * Produces a per-runtime id used to scope DOM attribute names and
5
- * telemetry tags. Single export by design — not a "guards" module.
5
+ * telemetry tags.
6
6
  */
7
7
  export declare function createRuntimeId(prefix?: string): string;
@@ -2,7 +2,7 @@
2
2
  * Stable runtime-instance id generator.
3
3
  *
4
4
  * Produces a per-runtime id used to scope DOM attribute names and
5
- * telemetry tags. Single export by design — not a "guards" module.
5
+ * telemetry tags.
6
6
  */
7
7
  export function createRuntimeId(prefix = "runtime") {
8
8
  return `${prefix}-${Math.random().toString(16).slice(2)}-${Date.now()}`;
@@ -35,10 +35,10 @@ export interface ShellScope {
35
35
  * answered the scope in its detail, so that runtime claims it by id. Once
36
36
  * the host has left the document, as in a teardown, the event is dispatched
37
37
  * on the runtime's element instead, which the scope captured when the
38
- * runtime answered. Events
39
- * sent before a runtime has answered are held, the newest
40
- * {@link MAX_HELD_SHELL_EVENTS}, and delivered in order once one does.
41
- * `disconnect` and `publish(null)` drop them.
38
+ * runtime answered. Events sent before a runtime has answered, or while the
39
+ * runtime's element is out of the document and the host is in it, are held,
40
+ * the newest {@link MAX_HELD_SHELL_EVENTS}, and delivered in order to the
41
+ * runtime that answers next. `disconnect` and `publish(null)` drop them.
42
42
  */
43
43
  send: (type: string, detail: object) => void;
44
44
  }
@@ -83,10 +83,14 @@ export function createShellScope() {
83
83
  if (runtime !== null)
84
84
  registration.sync(identity, runtime);
85
85
  };
86
+ // A runtime element that left the document while the shell stays in it no
87
+ // longer sits above the shell, so nothing would claim the shell's events.
88
+ const runtimeDeparted = (shellHost, address) => shellHost.isConnected &&
89
+ address.eventTarget.isConnected === false;
86
90
  function send(type, detail) {
87
91
  if (type === PIE_INTERNAL_CONTENT_LOADED_EVENT)
88
92
  loaded = detail;
89
- if (host && runtime !== null) {
93
+ if (host && runtime !== null && !runtimeDeparted(host, runtime)) {
90
94
  dispatchCrossBoundaryEvent(shellEventTarget(host, runtime), type, {
91
95
  ...detail,
92
96
  runtimeId: runtime.runtimeId,
@@ -97,20 +101,22 @@ export function createShellScope() {
97
101
  if (held.length > MAX_HELD_SHELL_EVENTS)
98
102
  held.shift();
99
103
  }
100
- // The subscription re-answers whenever the runtime republishes its context,
101
- // so only a different id means anything here.
104
+ // The subscription re-answers whenever the runtime republishes its context:
105
+ // under the same id it only delivers what the scope held meanwhile.
102
106
  function onRuntime(value) {
103
- if (value.runtimeId === runtime?.runtimeId)
104
- return;
105
- const moved = runtime !== null;
106
- runtime = { runtimeId: value.runtimeId, eventTarget: value.eventTarget };
107
- syncRegistration();
107
+ const moved = runtime !== null && value.runtimeId !== runtime.runtimeId;
108
+ if (value.runtimeId !== runtime?.runtimeId) {
109
+ runtime = { runtimeId: value.runtimeId, eventTarget: value.eventTarget };
110
+ syncRegistration();
111
+ }
108
112
  const pending = held;
109
113
  held = [];
110
114
  for (const event of pending)
111
115
  send(event.type, event.detail);
112
- if (moved && loaded)
116
+ const replayed = pending.some((event) => event.type === PIE_INTERNAL_CONTENT_LOADED_EVENT);
117
+ if (moved && loaded && !replayed) {
113
118
  send(PIE_INTERNAL_CONTENT_LOADED_EVENT, loaded);
119
+ }
114
120
  }
115
121
  function disconnect() {
116
122
  for (const provided of [shell, region])
@@ -2,10 +2,6 @@ import { type AssessmentToolkitHostRuntimeContext, type AssessmentToolkitRegionS
2
2
  import type { ZIndexLayer } from "../services/ToolCoordinator.js";
3
3
  import type { ToolCoordinatorApi } from "../services/interfaces.js";
4
4
  type BaseEventInit = Pick<EventInit, "bubbles" | "composed" | "cancelable">;
5
- /**
6
- * Contract helper for all toolkit/tool events that must cross custom-element boundaries.
7
- */
8
- export declare function createCrossBoundaryEvent<T>(name: string, detail: T, init?: Partial<BaseEventInit>): CustomEvent<T>;
9
5
  /**
10
6
  * Dispatches a cross-boundary event with toolkit defaults.
11
7
  */
@@ -8,7 +8,7 @@ const CROSS_BOUNDARY_EVENT_INIT = {
8
8
  /**
9
9
  * Contract helper for all toolkit/tool events that must cross custom-element boundaries.
10
10
  */
11
- export function createCrossBoundaryEvent(name, detail, init = {}) {
11
+ function createCrossBoundaryEvent(name, detail, init = {}) {
12
12
  return new CustomEvent(name, {
13
13
  detail,
14
14
  ...CROSS_BOUNDARY_EVENT_INIT,
@@ -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);