@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
@@ -19,6 +19,7 @@ export class PnpPolicySource {
19
19
  const allSupports = new Set();
20
20
  pnp?.supports?.forEach((s) => allSupports.add(s));
21
21
  pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
22
+ pnp?.activateAtInit?.forEach((s) => allSupports.add(s));
22
23
  districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
23
24
  districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
24
25
  for (const s of Object.keys(testAdmin?.toolOverrides ?? {})) {
@@ -42,8 +43,8 @@ export class PnpPolicySource {
42
43
  * feature: the returned result carries exactly one decision, and
43
44
  * `decisions[0].action` is the verdict.
44
45
  *
45
- * Reusing `resolveSupport(...)` rather than re-walking the precedence rules
46
- * is the point — a second copy of the eight levels would drift.
46
+ * It reuses `resolveSupport(...)` because a second copy of the eight levels
47
+ * would drift.
47
48
  */
48
49
  resolveFeature(featureId, args) {
49
50
  const { ctx, result } = this.prepare(args);
@@ -61,10 +62,9 @@ export class PnpPolicySource {
61
62
  const itemSettings = item?.settings;
62
63
  const result = {
63
64
  blockedToolIds: new Set(),
64
- mandatedToolIds: new Set(),
65
+ mandates: new Map(),
65
66
  perToolFlags: new Map(),
66
67
  decisions: [],
67
- unmappedSupportIds: this.unmappedIds(pnp, settings, itemSettings),
68
68
  blockedOverrides: new Map(),
69
69
  sources: {},
70
70
  };
@@ -94,7 +94,6 @@ export class PnpPolicySource {
94
94
  districtPolicy: settings?.districtPolicy,
95
95
  testAdmin: settings?.testAdministration,
96
96
  itemSettings,
97
- toolConfigs: settings?.toolConfigs,
98
97
  };
99
98
  return { ctx, result };
100
99
  }
@@ -164,13 +163,21 @@ export class PnpPolicySource {
164
163
  });
165
164
  return;
166
165
  }
166
+ const activateAtInit = ctx.pnp?.activateAtInit?.includes(supportId) === true;
167
+ const mandate = ctx.itemSettings?.requiredTools?.includes(supportId)
168
+ ? "item-requirement"
169
+ : ctx.districtPolicy?.requiredTools?.includes(supportId)
170
+ ? "district-requirement"
171
+ : undefined;
167
172
  // 5. Test administration override `true`: grants the support for the
168
- // session.
173
+ // session. A requirement naming it keeps its mandate.
169
174
  if (override === true) {
175
+ if (mandate)
176
+ out.mandates.set(supportId, mandate);
170
177
  out.perToolFlags.set(supportId, {
171
- required: false,
178
+ required: mandate !== undefined,
172
179
  alwaysAvailable: true,
173
- settings: this.resolveToolSettings(supportId, ctx),
180
+ activateAtInit,
174
181
  rule: "test-admin-override",
175
182
  });
176
183
  out.decisions.push({
@@ -185,12 +192,12 @@ export class PnpPolicySource {
185
192
  return;
186
193
  }
187
194
  // 6. Item requirement (forces enable)
188
- if (ctx.itemSettings?.requiredTools?.includes(supportId)) {
189
- out.mandatedToolIds.add(supportId);
195
+ if (mandate === "item-requirement") {
196
+ out.mandates.set(supportId, mandate);
190
197
  out.perToolFlags.set(supportId, {
191
198
  required: true,
192
199
  alwaysAvailable: false,
193
- settings: this.resolveToolSettings(supportId, ctx),
200
+ activateAtInit,
194
201
  rule: "item-requirement",
195
202
  });
196
203
  out.decisions.push({
@@ -200,17 +207,17 @@ export class PnpPolicySource {
200
207
  action: "enable",
201
208
  sourceType: "item",
202
209
  reason: `Item requires "${supportId}" for this question`,
203
- value: ctx.itemSettings.requiredTools,
210
+ value: ctx.itemSettings?.requiredTools,
204
211
  });
205
212
  return;
206
213
  }
207
214
  // 7. District requirement
208
- if (ctx.districtPolicy?.requiredTools?.includes(supportId)) {
209
- out.mandatedToolIds.add(supportId);
215
+ if (mandate === "district-requirement") {
216
+ out.mandates.set(supportId, mandate);
210
217
  out.perToolFlags.set(supportId, {
211
218
  required: true,
212
219
  alwaysAvailable: false,
213
- settings: this.resolveToolSettings(supportId, ctx),
220
+ activateAtInit,
214
221
  rule: "district-requirement",
215
222
  });
216
223
  out.decisions.push({
@@ -220,16 +227,35 @@ export class PnpPolicySource {
220
227
  action: "enable",
221
228
  sourceType: "assessment",
222
229
  reason: `District policy requires "${supportId}" for all assessments`,
223
- value: ctx.districtPolicy.requiredTools,
230
+ value: ctx.districtPolicy?.requiredTools,
231
+ });
232
+ return;
233
+ }
234
+ // 8. Profile supports (student needs). `activateAtInit` grants as
235
+ // `supports` does and names the tool to turn on at start.
236
+ if (activateAtInit) {
237
+ out.perToolFlags.set(supportId, {
238
+ required: false,
239
+ alwaysAvailable: true,
240
+ activateAtInit,
241
+ rule: "pnp-activate-at-init",
242
+ });
243
+ out.decisions.push({
244
+ precedence: 8,
245
+ rule: "pnp-activate-at-init",
246
+ featureId: supportId,
247
+ action: "enable",
248
+ sourceType: "student",
249
+ reason: `Student PNP profile turns on "${supportId}" at start`,
250
+ value: ctx.pnp?.activateAtInit,
224
251
  });
225
252
  return;
226
253
  }
227
- // 8. Profile supports (student needs)
228
254
  if (ctx.pnp?.supports?.includes(supportId)) {
229
255
  out.perToolFlags.set(supportId, {
230
256
  required: false,
231
257
  alwaysAvailable: true,
232
- settings: this.resolveToolSettings(supportId, ctx),
258
+ activateAtInit: false,
233
259
  rule: "pnp-support",
234
260
  });
235
261
  out.decisions.push({
@@ -256,24 +282,31 @@ export class PnpPolicySource {
256
282
  });
257
283
  }
258
284
  /**
259
- * The named ids no tool is registered under, with the rules naming each in
260
- * precedence order. A support id is the id of the tool it grants, so an id
261
- * missing from the registry matches nothing placed.
285
+ * Ids the assessment or any of `items` names that no tool is registered
286
+ * under, each with the rules whose lists name it, in precedence order. A
287
+ * support id is the id of the tool it grants, so an id missing from the
288
+ * registry matches nothing placed; the engine turns each into a
289
+ * `tool-policy.unknownSupportId` diagnostic. Empty when the registry is: an
290
+ * empty registry has nothing to check an id against, and
291
+ * `tool-config-validation` already reports it once.
262
292
  */
263
- unmappedIds(pnp, settings, itemSettings) {
293
+ unknownSupportIds(assessment, items) {
264
294
  const unmapped = new Map();
265
295
  if (this.toolRegistry.getAllTools().length === 0)
266
296
  return unmapped;
297
+ const pnp = assessment?.personalNeedsProfile;
298
+ const settings = assessment?.settings;
267
299
  const named = [
268
300
  ["district-block", settings?.districtPolicy?.blockedTools],
269
301
  [
270
302
  "test-admin-override",
271
303
  Object.keys(settings?.testAdministration?.toolOverrides ?? {}),
272
304
  ],
273
- ["item-restriction", itemSettings?.restrictedTools],
305
+ ["item-restriction", items.flatMap((item) => item.restrictedTools ?? [])],
274
306
  ["pnp-prohibited", pnp?.prohibitedSupports],
275
- ["item-requirement", itemSettings?.requiredTools],
307
+ ["item-requirement", items.flatMap((item) => item.requiredTools ?? [])],
276
308
  ["district-requirement", settings?.districtPolicy?.requiredTools],
309
+ ["pnp-activate-at-init", pnp?.activateAtInit],
277
310
  ["pnp-support", pnp?.supports],
278
311
  ];
279
312
  for (const [rule, ids] of named) {
@@ -288,9 +321,4 @@ export class PnpPolicySource {
288
321
  }
289
322
  return unmapped;
290
323
  }
291
- /** Tool parameters: the item's entry, else the assessment's. */
292
- resolveToolSettings(supportId, ctx) {
293
- return (ctx.itemSettings?.toolParameters?.[supportId] ??
294
- ctx.toolConfigs?.[supportId]);
295
- }
296
324
  }
@@ -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,