@pie-players/pie-assessment-toolkit 0.3.30 → 0.3.32

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 (179) hide show
  1. package/README.md +289 -5
  2. package/dist/components/ItemToolBar.custom-element.js +165 -92
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +604 -107
  4. package/dist/components/SectionToolBar.custom-element.js +0 -36
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/policy/core/PolicySource.d.ts +64 -0
  9. package/dist/policy/core/PolicySource.d.ts.map +1 -0
  10. package/dist/policy/core/PolicySource.js +17 -0
  11. package/dist/policy/core/PolicySource.js.map +1 -0
  12. package/dist/policy/core/ToolPolicyEngine.d.ts +123 -0
  13. package/dist/policy/core/ToolPolicyEngine.d.ts.map +1 -0
  14. package/dist/policy/core/ToolPolicyEngine.js +217 -0
  15. package/dist/policy/core/ToolPolicyEngine.js.map +1 -0
  16. package/dist/policy/core/compose-decision.d.ts +37 -0
  17. package/dist/policy/core/compose-decision.d.ts.map +1 -0
  18. package/dist/policy/core/compose-decision.js +332 -0
  19. package/dist/policy/core/compose-decision.js.map +1 -0
  20. package/dist/policy/core/decision-types.d.ts +142 -0
  21. package/dist/policy/core/decision-types.d.ts.map +1 -0
  22. package/dist/policy/core/decision-types.js +13 -0
  23. package/dist/policy/core/decision-types.js.map +1 -0
  24. package/dist/policy/core/engine-context.d.ts +30 -0
  25. package/dist/policy/core/engine-context.d.ts.map +1 -0
  26. package/dist/policy/core/engine-context.js +28 -0
  27. package/dist/policy/core/engine-context.js.map +1 -0
  28. package/dist/policy/core/policy-source-tag.d.ts +32 -0
  29. package/dist/policy/core/policy-source-tag.d.ts.map +1 -0
  30. package/dist/policy/core/policy-source-tag.js +29 -0
  31. package/dist/policy/core/policy-source-tag.js.map +1 -0
  32. package/dist/policy/core/provenance.d.ts +163 -0
  33. package/dist/policy/core/provenance.d.ts.map +1 -0
  34. package/dist/policy/core/provenance.js +232 -0
  35. package/dist/policy/core/provenance.js.map +1 -0
  36. package/dist/policy/core/qti-inputs.d.ts +67 -0
  37. package/dist/policy/core/qti-inputs.d.ts.map +1 -0
  38. package/dist/policy/core/qti-inputs.js +139 -0
  39. package/dist/policy/core/qti-inputs.js.map +1 -0
  40. package/dist/policy/engine.d.ts +28 -0
  41. package/dist/policy/engine.d.ts.map +1 -0
  42. package/dist/policy/engine.js +24 -0
  43. package/dist/policy/engine.js.map +1 -0
  44. package/dist/policy/internal.d.ts +26 -0
  45. package/dist/policy/internal.d.ts.map +1 -0
  46. package/dist/policy/internal.js +26 -0
  47. package/dist/policy/internal.js.map +1 -0
  48. package/dist/policy/sources/QtiPolicySource.d.ts +146 -0
  49. package/dist/policy/sources/QtiPolicySource.d.ts.map +1 -0
  50. package/dist/policy/sources/QtiPolicySource.js +270 -0
  51. package/dist/policy/sources/QtiPolicySource.js.map +1 -0
  52. package/dist/runtime/SectionRuntimeEngine.d.ts +145 -0
  53. package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -1
  54. package/dist/runtime/SectionRuntimeEngine.js +164 -3
  55. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  56. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +144 -0
  57. package/dist/runtime/adapter/SectionEngineAdapter.d.ts.map +1 -0
  58. package/dist/runtime/adapter/SectionEngineAdapter.js +183 -0
  59. package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -0
  60. package/dist/runtime/adapter/coordinator-bridge.d.ts +93 -0
  61. package/dist/runtime/adapter/coordinator-bridge.d.ts.map +1 -0
  62. package/dist/runtime/adapter/coordinator-bridge.js +127 -0
  63. package/dist/runtime/adapter/coordinator-bridge.js.map +1 -0
  64. package/dist/runtime/adapter/dom-event-bridge.d.ts +63 -0
  65. package/dist/runtime/adapter/dom-event-bridge.d.ts.map +1 -0
  66. package/dist/runtime/adapter/dom-event-bridge.js +95 -0
  67. package/dist/runtime/adapter/dom-event-bridge.js.map +1 -0
  68. package/dist/runtime/adapter/framework-error-bridge.d.ts +37 -0
  69. package/dist/runtime/adapter/framework-error-bridge.d.ts.map +1 -0
  70. package/dist/runtime/adapter/framework-error-bridge.js +47 -0
  71. package/dist/runtime/adapter/framework-error-bridge.js.map +1 -0
  72. package/dist/runtime/adapter/instrumentation-bridge.d.ts +32 -0
  73. package/dist/runtime/adapter/instrumentation-bridge.d.ts.map +1 -0
  74. package/dist/runtime/adapter/instrumentation-bridge.js +39 -0
  75. package/dist/runtime/adapter/instrumentation-bridge.js.map +1 -0
  76. package/dist/runtime/adapter/subscriber-fanout.d.ts +35 -0
  77. package/dist/runtime/adapter/subscriber-fanout.d.ts.map +1 -0
  78. package/dist/runtime/adapter/subscriber-fanout.js +60 -0
  79. package/dist/runtime/adapter/subscriber-fanout.js.map +1 -0
  80. package/dist/runtime/core/SectionEngineCore.d.ts +52 -0
  81. package/dist/runtime/core/SectionEngineCore.d.ts.map +1 -0
  82. package/dist/runtime/core/SectionEngineCore.js +81 -0
  83. package/dist/runtime/core/SectionEngineCore.js.map +1 -0
  84. package/dist/runtime/core/cohort.d.ts +40 -0
  85. package/dist/runtime/core/cohort.d.ts.map +1 -0
  86. package/dist/runtime/core/cohort.js +49 -0
  87. package/dist/runtime/core/cohort.js.map +1 -0
  88. package/dist/runtime/core/engine-input.d.ts +114 -0
  89. package/dist/runtime/core/engine-input.d.ts.map +1 -0
  90. package/dist/runtime/core/engine-input.js +19 -0
  91. package/dist/runtime/core/engine-input.js.map +1 -0
  92. package/dist/runtime/core/engine-output.d.ts +62 -0
  93. package/dist/runtime/core/engine-output.d.ts.map +1 -0
  94. package/dist/runtime/core/engine-output.js +27 -0
  95. package/dist/runtime/core/engine-output.js.map +1 -0
  96. package/dist/runtime/core/engine-readiness.d.ts +49 -0
  97. package/dist/runtime/core/engine-readiness.d.ts.map +1 -0
  98. package/dist/runtime/core/engine-readiness.js +47 -0
  99. package/dist/runtime/core/engine-readiness.js.map +1 -0
  100. package/dist/runtime/core/engine-resolver.d.ts +172 -0
  101. package/dist/runtime/core/engine-resolver.d.ts.map +1 -0
  102. package/dist/runtime/core/engine-resolver.js +170 -0
  103. package/dist/runtime/core/engine-resolver.js.map +1 -0
  104. package/dist/runtime/core/engine-stage-derivation.d.ts +19 -0
  105. package/dist/runtime/core/engine-stage-derivation.d.ts.map +1 -0
  106. package/dist/runtime/core/engine-stage-derivation.js +30 -0
  107. package/dist/runtime/core/engine-stage-derivation.js.map +1 -0
  108. package/dist/runtime/core/engine-state.d.ts +83 -0
  109. package/dist/runtime/core/engine-state.d.ts.map +1 -0
  110. package/dist/runtime/core/engine-state.js +49 -0
  111. package/dist/runtime/core/engine-state.js.map +1 -0
  112. package/dist/runtime/core/engine-transition.d.ts +49 -0
  113. package/dist/runtime/core/engine-transition.d.ts.map +1 -0
  114. package/dist/runtime/core/engine-transition.js +261 -0
  115. package/dist/runtime/core/engine-transition.js.map +1 -0
  116. package/dist/runtime/engine-context.d.ts +40 -0
  117. package/dist/runtime/engine-context.d.ts.map +1 -0
  118. package/dist/runtime/engine-context.js +32 -0
  119. package/dist/runtime/engine-context.js.map +1 -0
  120. package/dist/runtime/engine.d.ts +24 -0
  121. package/dist/runtime/engine.d.ts.map +1 -0
  122. package/dist/runtime/engine.js +24 -0
  123. package/dist/runtime/engine.js.map +1 -0
  124. package/dist/runtime/internal.d.ts +44 -0
  125. package/dist/runtime/internal.d.ts.map +1 -0
  126. package/dist/runtime/internal.js +46 -0
  127. package/dist/runtime/internal.js.map +1 -0
  128. package/dist/runtime/runtime-id.d.ts +8 -0
  129. package/dist/runtime/runtime-id.d.ts.map +1 -0
  130. package/dist/runtime/runtime-id.js +10 -0
  131. package/dist/runtime/runtime-id.js.map +1 -0
  132. package/dist/runtime/section-runtime-engine-host-context.d.ts +74 -0
  133. package/dist/runtime/section-runtime-engine-host-context.d.ts.map +1 -0
  134. package/dist/runtime/section-runtime-engine-host-context.js +96 -0
  135. package/dist/runtime/section-runtime-engine-host-context.js.map +1 -0
  136. package/dist/runtime/stage-emit-gate.d.ts +93 -0
  137. package/dist/runtime/stage-emit-gate.d.ts.map +1 -0
  138. package/dist/runtime/stage-emit-gate.js +83 -0
  139. package/dist/runtime/stage-emit-gate.js.map +1 -0
  140. package/dist/services/ToolkitCoordinator.d.ts +280 -7
  141. package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
  142. package/dist/services/ToolkitCoordinator.js +412 -32
  143. package/dist/services/ToolkitCoordinator.js.map +1 -1
  144. package/dist/services/framework-error-bus.d.ts +73 -0
  145. package/dist/services/framework-error-bus.d.ts.map +1 -0
  146. package/dist/services/framework-error-bus.js +68 -0
  147. package/dist/services/framework-error-bus.js.map +1 -0
  148. package/dist/services/framework-error.d.ts +36 -1
  149. package/dist/services/framework-error.d.ts.map +1 -1
  150. package/dist/services/framework-error.js +37 -0
  151. package/dist/services/framework-error.js.map +1 -1
  152. package/dist/services/interfaces.d.ts +78 -0
  153. package/dist/services/interfaces.d.ts.map +1 -1
  154. package/dist/services/interfaces.js.map +1 -1
  155. package/dist/services/section-controller-types.d.ts +20 -0
  156. package/dist/services/section-controller-types.d.ts.map +1 -1
  157. package/dist/services/tools-config-normalizer.d.ts +23 -0
  158. package/dist/services/tools-config-normalizer.d.ts.map +1 -1
  159. package/dist/services/tools-config-normalizer.js +13 -1
  160. package/dist/services/tools-config-normalizer.js.map +1 -1
  161. package/dist/tools/client.d.ts +0 -1
  162. package/dist/tools/client.d.ts.map +1 -1
  163. package/dist/tools/client.js +0 -2
  164. package/dist/tools/client.js.map +1 -1
  165. package/dist/tools/types.d.ts +0 -26
  166. package/dist/tools/types.d.ts.map +1 -1
  167. package/package.json +23 -7
  168. package/dist/runtime/runtime-event-guards.d.ts +0 -2
  169. package/dist/runtime/runtime-event-guards.d.ts.map +0 -1
  170. package/dist/runtime/runtime-event-guards.js +0 -4
  171. package/dist/runtime/runtime-event-guards.js.map +0 -1
  172. package/dist/tools/tool-coordinator.d.ts +0 -72
  173. package/dist/tools/tool-coordinator.d.ts.map +0 -1
  174. package/dist/tools/tool-coordinator.js +0 -194
  175. package/dist/tools/tool-coordinator.js.map +0 -1
  176. package/dist/types/events.d.ts +0 -157
  177. package/dist/types/events.d.ts.map +0 -1
  178. package/dist/types/events.js +0 -11
  179. package/dist/types/events.js.map +0 -1
@@ -16,10 +16,12 @@ const __pieDefineSafely = (tagName, ctor) => {
16
16
 
17
17
  import 'svelte/internal/disclose-version';
18
18
  import * as $ from 'svelte/internal/client';
19
+ import { untrack } from "svelte";
19
20
  import { ContextProvider, ContextRoot, requestContext } from "@pie-players/pie-context";
20
21
 
21
22
  import {
22
23
  attachInstrumentationEventBridge,
24
+ createStageTracker,
23
25
  resolveInstrumentationProvider,
24
26
  TOOLKIT_INSTRUMENTATION_EVENT_MAP
25
27
  } from "@pie-players/pie-players-shared/pie";
@@ -34,7 +36,9 @@ import {
34
36
  import { connectAssessmentToolkitHostRuntimeContext } from "../context/runtime-context-consumer.js";
35
37
  import { ToolkitCoordinator } from "../services/ToolkitCoordinator.js";
36
38
  import { formatFrameworkErrorForConsole, frameworkErrorFromUnknown } from "../services/framework-error.js";
39
+ import { FrameworkErrorBus } from "../services/framework-error-bus.js";
37
40
  import { normalizeAndValidateToolsConfig } from "../services/tool-config-validation.js";
41
+ import { parseToolList } from "../services/tools-config-normalizer.js";
38
42
 
39
43
  import {
40
44
  PIE_INTERNAL_CONTENT_LOADED_EVENT,
@@ -46,7 +50,9 @@ import {
46
50
 
47
51
  import { dispatchCrossBoundaryEvent } from "../runtime/tool-host-contract.js";
48
52
  import { SectionRuntimeEngine } from "../runtime/SectionRuntimeEngine.js";
49
- import { createRuntimeId } from "../runtime/runtime-event-guards.js";
53
+ import { connectSectionRuntimeEngineHostContext } from "../runtime/section-runtime-engine-host-context.js";
54
+ import { runStageEmitWithSuppression } from "../runtime/stage-emit-gate.js";
55
+ import { createRuntimeId } from "../runtime/runtime-id.js";
50
56
 
51
57
  import {
52
58
  createSessionEmitPolicyState,
@@ -68,6 +74,31 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
68
74
 
69
75
  const runtimeId = createRuntimeId("toolkit");
70
76
  const sectionEngine = new SectionRuntimeEngine();
77
+
78
+ // M7 PR 6: when wrapped by a section-player layout, the kernel
79
+ // publishes its engine via the cross-CE
80
+ // `sectionRuntimeEngineHostContext`. We track the upstream
81
+ // reference so wrapped-mode-specific behavior (stage-emit
82
+ // suppression to avoid double-emit on layout CE; framework-error
83
+ // FSM input forwarding) can branch off it. `null` means
84
+ // standalone — no upstream provider responded — and the toolkit
85
+ // keeps its existing pre-PR-6 behavior. The reference is set once
86
+ // per cohort by the cross-CE consumer effect below; it never
87
+ // becomes a substitute for `sectionEngine` (the local engine
88
+ // remains the controller-side facade for `register`,
89
+ // `handleContent*`, `initialize`, etc.). PR 7+ will collapse the
90
+ // local controller-side surface onto the upstream engine.
91
+ let upstreamEngine = $.state(null);
92
+
93
+ // Per-CE-instance framework-error bus. Shared with the owned
94
+ // `ToolkitCoordinator` so coordinator-side failures (provider-init,
95
+ // tts-init, ...) flow through the same fan-out as CE-side failures
96
+ // (coordinator-init, runtime-init, runtime-dispose). The bus is the
97
+ // single source for the canonical `onFrameworkError` prop, the
98
+ // `framework-error` DOM event, the banner UI, and the
99
+ // `subscribeFrameworkErrors` API on the coordinator.
100
+ const frameworkErrorBus = new FrameworkErrorBus();
101
+
71
102
  const DEFAULT_ENV = { mode: "gather", role: "student" };
72
103
 
73
104
  const DEFAULT_ITEM_PLAYER_BY_TYPE = {
@@ -81,27 +112,46 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
81
112
  section = $.prop($$props, 'section', 7, null),
82
113
  sectionId = $.prop($$props, 'sectionId', 7, ""),
83
114
  attemptId = $.prop($$props, 'attemptId', 7, ""),
84
- view = $.prop($$props, 'view', 7, "candidate"),
85
115
  env = $.prop($$props, 'env', 23, () => ({})),
86
116
  lazyInit = $.prop($$props, 'lazyInit', 7, true),
87
117
  toolConfigStrictness = $.prop($$props, 'toolConfigStrictness', 7, "error"),
88
118
  tools = $.prop($$props, 'tools', 23, () => ({})),
119
+ enabledTools = $.prop($$props, 'enabledTools', 7, ""),
89
120
  toolRegistry = $.prop($$props, 'toolRegistry', 7, null),
90
121
  accessibility = $.prop($$props, 'accessibility', 23, () => ({})),
91
122
  player = $.prop($$props, 'player', 7, null),
92
123
  playerType = $.prop($$props, 'playerType', 7, ""),
93
124
  coordinator = $.prop($$props, 'coordinator', 7, null),
94
125
  createSectionController = $.prop($$props, 'createSectionController', 7, null),
95
- frameworkErrorCallback = $.prop($$props, 'frameworkErrorHook', 7, null),
96
126
  onFrameworkError = $.prop($$props, 'onFrameworkError', 7, null),
97
- onframeworkerror = $.prop($$props, 'onframeworkerror', 7, null),
98
127
  errorRenderer = $.prop($$props, 'errorRenderer', 7, null),
128
+ onStageChange = $.prop($$props, 'onStageChange', 7, null),
129
+ // M8 PR 2 — additive Tool Policy Engine inputs. See the
130
+ // `<svelte:options>` props block above for the rationale.
131
+ // `qtiEnforcement` accepts `"on"`, `"off"`, or `null` (auto —
132
+ // the coordinator infers the effective mode from QTI material
133
+ // on the bound `assessment` / `currentItemRef`). Embedded
134
+ // under `<pie-section-player-*>` the same override flows via
135
+ // `runtime.tools.qtiEnforcement`; the effect below falls back
136
+ // to `tools.qtiEnforcement` when the explicit prop is `null`
137
+ // so both entry points converge on the same coordinator call.
138
+ assessment = $.prop($$props, 'assessment', 7, null),
139
+ currentItemRef = $.prop($$props, 'currentItemRef', 7, null),
140
+ qtiEnforcement = $.prop($$props, 'qtiEnforcement', 7, null),
99
141
  isolation = $.prop($$props, 'isolation', 7, "inherit");
100
142
 
101
143
  let anchor = $.state(null);
102
144
  let ownedCoordinator = $.state(null);
103
145
  let inheritedRuntime = $.state(null);
104
- let lastOwnership = $.state(null);
146
+
147
+ // `lastOwnership` is a self-comparison latch read and written only by
148
+ // the ownership-change `$effect` below. Keeping it as `$state` would
149
+ // make Svelte treat the same-effect read+write as a reactivity loop
150
+ // (effect_update_depth_exceeded). It does not feed any other reactive
151
+ // consumer, so a plain `let` is safer and matches the canonical Svelte
152
+ // 5 latch pattern from `.cursor/rules/svelte-subscription-safety.mdc`.
153
+ let lastOwnership = null;
154
+
105
155
  let provider = null;
106
156
  let contextRoot = null;
107
157
  let hostRuntimeProvider = null;
@@ -112,8 +162,17 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
112
162
  let frameworkErrorModel = $.state(null);
113
163
  let frameworkErrorTitle = $.state("Unable to initialize assessment toolkit.");
114
164
  let frameworkErrorDetails = $.state($.proxy([]));
115
- let deliveredFrameworkErrorKey = $.state("");
116
- let lastOwnedBootstrapFailureKey = $.state("");
165
+
166
+ // Self-comparison latches for the framework-error redelivery `$effect`
167
+ // and the owned-coordinator bootstrap `$effect` below. Same rationale
168
+ // as `lastOwnership`: keeping these as `$state` made the owning
169
+ // effects read+write the same source in a single pass, tripping
170
+ // `effect_update_depth_exceeded`. Neither value is read by any other
171
+ // reactive consumer, so plain `let` removes the loop without dropping
172
+ // any subscriber.
173
+ let deliveredFrameworkErrorKey = "";
174
+
175
+ let lastOwnedBootstrapFailureKey = "";
117
176
  let lastCompositionRevisionKey = $.state("");
118
177
  let pendingCompositionModel = null;
119
178
  let pendingCompositionEmit = false;
@@ -121,6 +180,79 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
121
180
  let pendingCrossBoundaryEvents = [];
122
181
  const sessionEmitPolicyState = createSessionEmitPolicyState();
123
182
 
183
+ // M6 canonical stage tracker. Post-retro the toolkit applies the
184
+ // same four-stage canonical list (`composed`, `engine-ready`,
185
+ // `interactive`, `disposed`) as every layout CE; subscriber
186
+ // iteration order stays stable across `<pie-section-player-*>` and
187
+ // `<pie-assessment-toolkit>` CE shapes. Cohort changes
188
+ // (`(sectionId, attemptId)`) reset the tracker so the new cohort
189
+ // begins emitting from `composed`. Initial seed values are captured
190
+ // via `untrack` because the tracker explicitly absorbs subsequent
191
+ // cohort changes through `reset()`, not through prop reactivity at
192
+ // construction time.
193
+ const stageTracker = createStageTracker({
194
+ sourceCe: "pie-assessment-toolkit",
195
+ sourceCeShape: "toolkit",
196
+ runtimeId,
197
+ sectionId: untrack(() => sectionId() || undefined),
198
+ attemptId: untrack(() => attemptId() || undefined),
199
+ emit: (detail) => {
200
+ // M7 PR 6: route the toolkit's stage-emit through the
201
+ // extracted `runStageEmitWithSuppression` helper so the
202
+ // wrapped/standalone gating logic is testable in isolation
203
+ // (see `tests/runtime/stage-emit-gate.test.ts`). The
204
+ // thunks below preserve pre-PR-6 semantics:
205
+ //
206
+ // - `isSuppressed`: when wrapped by a section-player
207
+ // layout the kernel's engine emits the canonical
208
+ // `pie-stage-change` chain on the layout CE host and
209
+ // invokes the runtime-tier `onStageChange` callback
210
+ // itself. Suppressing here prevents the toolkit CE
211
+ // from also emitting (which would bubble through
212
+ // `composed: true` to the layout CE host and produce a
213
+ // duplicate emission for outside listeners). The
214
+ // internal stage-tracker latch state advances before
215
+ // `opts.emit` runs, so suppression only short-circuits
216
+ // externally-visible side effects — the toolkit's own
217
+ // readiness path (`waitUntilReady` / `engine-ready` /
218
+ // `interactive`) stays correct in standalone mode and
219
+ // degenerates into a series of suppressed emits in
220
+ // wrapped mode without losing any latch transitions.
221
+ //
222
+ // The reactive `upstreamEngine` is read inside the
223
+ // thunk (i.e. at emit time, not at construction time)
224
+ // and all callers of `stageTracker.enter(...)` are
225
+ // themselves wrapped in `untrack(...)` (see effects
226
+ // below), so the read does not feed back into any
227
+ // tracked dependency graph.
228
+ //
229
+ // - `getOnStageChange`: the prop is read on every emit
230
+ // so reassignments (cohort change, host swap) always
231
+ // reach the latest handler.
232
+ runStageEmitWithSuppression(
233
+ {
234
+ isSuppressed: () => $.get(upstreamEngine) !== null,
235
+ dispatchDomEvent: (d) => emit("pie-stage-change", d),
236
+ getOnStageChange: () => onStageChange()
237
+ },
238
+ detail
239
+ );
240
+ }
241
+ });
242
+
243
+ let lastStageCohortKey = $.state($.proxy(untrack(() => `${sectionId()}|${attemptId()}`)));
244
+ let composedStageEntered = $.state(false);
245
+ let engineReadyStageEntered = $.state(false);
246
+ let interactiveStageEntered = $.state(false);
247
+
248
+ // `sectionInitialized` is the second of two preconditions for the
249
+ // `interactive` stage emission. The toolkit's interactive entrance
250
+ // requires *both* `engine-ready` (coordinator bring-up settled) and
251
+ // `sectionInitialized` (section composition + controller hydrated)
252
+ // before it fires. A reactive effect below joins them so neither
253
+ // race ordering silently drops a stage.
254
+ let sectionInitialized = $.state(false);
255
+
124
256
  function getHostElement() {
125
257
  if (!$.get(anchor)) return null;
126
258
 
@@ -200,6 +332,20 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
200
332
  }
201
333
  }
202
334
 
335
+ /**
336
+ * Build a `FrameworkErrorModel` and publish it on this CE's
337
+ * framework-error bus.
338
+ *
339
+ * All delivery side-effects (banner UI, `framework-error` DOM event,
340
+ * hook delivery, console logging) live in the bus subscriber
341
+ * registered below — keeping this helper a thin publish-side wrapper
342
+ * that CE-internal callers can use.
343
+ *
344
+ * Coordinator-side failures (provider-init, tts-init, ...) reach the
345
+ * same subscriber by way of the shared bus (passed into the owned
346
+ * coordinator via `buildOwnedCoordinator`), so consumers see one
347
+ * canonical fan-out per error.
348
+ */
203
349
  function reportFrameworkError(args) {
204
350
  const model = frameworkErrorFromUnknown({
205
351
  kind: args.kind,
@@ -208,49 +354,69 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
208
354
  recoverable: args.recoverable
209
355
  });
210
356
 
211
- console.error(formatFrameworkErrorForConsole(model), model.cause);
357
+ frameworkErrorBus.reportFrameworkError(model);
212
358
 
213
- const rendered = applyErrorRenderer(model);
359
+ return model;
360
+ }
214
361
 
215
- $.set(frameworkErrorModel, model, true);
216
- $.set(frameworkErrorTitle, rendered.title, true);
217
- $.set(frameworkErrorDetails, rendered.details, true);
218
- emit("framework-error", model);
219
- emit("runtime-error", { runtimeId, error: args.error, frameworkError: model });
362
+ /**
363
+ * Whether this CE should surface the bootstrap banner for `kind`.
364
+ *
365
+ * The banner is the visible "we could not start the toolkit" surface
366
+ * and is intentionally narrower than the canonical contract: it only
367
+ * fires for fatal CE-bootstrap kinds (`coordinator-init`,
368
+ * `runtime-init`, `tool-config`). Coordinator-internal failures
369
+ * (provider-init, tts-init, ...) still flow through the canonical
370
+ * hook + DOM event but do not replace the toolkit content with an
371
+ * error card — they are degradations, not bootstrap failures.
372
+ */
373
+ function isBootstrapKind(kind) {
374
+ return kind === "coordinator-init" || kind === "runtime-init" || kind === "tool-config";
375
+ }
220
376
 
221
- const frameworkErrorHook = frameworkErrorCallback() ?? onFrameworkError() ?? onframeworkerror();
377
+ function deliverFrameworkErrorHook(model) {
378
+ if (!onFrameworkError()) return;
222
379
 
223
- if (frameworkErrorHook) {
224
- try {
225
- frameworkErrorHook(model);
226
- $.set(deliveredFrameworkErrorKey, `${model.kind}|${model.source}|${model.message}`);
227
- } catch(hookError) {
228
- console.error(`[pie-framework:runtime-init:pie-assessment-toolkit] framework error hook failed`, hookError);
229
- }
380
+ try {
381
+ onFrameworkError()(model);
382
+ deliveredFrameworkErrorKey = `${model.kind}|${model.source}|${model.message}`;
383
+ } catch(hookError) {
384
+ console.error(`[pie-framework:${model.kind}:${model.source}] framework error hook failed`, hookError);
230
385
  }
231
-
232
- return model;
233
386
  }
234
387
 
235
388
  $.user_effect(() => {
236
- if (!$.get(frameworkErrorModel)) {
237
- return;
238
- }
389
+ const detach = frameworkErrorBus.subscribeFrameworkErrors((model) => {
390
+ console.error(formatFrameworkErrorForConsole(model), model.cause);
239
391
 
240
- const frameworkErrorKey = `${$.get(frameworkErrorModel).kind}|${$.get(frameworkErrorModel).source}|${$.get(frameworkErrorModel).message}`;
392
+ if (isBootstrapKind(model.kind)) {
393
+ const rendered = applyErrorRenderer(model);
241
394
 
242
- if ($.get(deliveredFrameworkErrorKey) === frameworkErrorKey) return;
395
+ $.set(frameworkErrorModel, model, true);
396
+ $.set(frameworkErrorTitle, rendered.title, true);
397
+ $.set(frameworkErrorDetails, rendered.details, true);
398
+ }
243
399
 
244
- const frameworkErrorHook = frameworkErrorCallback() ?? onFrameworkError() ?? onframeworkerror();
400
+ emit("framework-error", model);
401
+ deliverFrameworkErrorHook(model);
402
+ });
245
403
 
246
- if (!frameworkErrorHook) return;
404
+ return () => {
405
+ detach();
406
+ };
407
+ });
247
408
 
248
- try {
249
- frameworkErrorHook($.get(frameworkErrorModel));
250
- $.set(deliveredFrameworkErrorKey, frameworkErrorKey);
251
- } catch(hookError) {
252
- console.error(`[pie-framework:runtime-init:pie-assessment-toolkit] framework error hook failed`, hookError);
253
- }
409
+ $.user_effect(() => {
410
+ if (!$.get(frameworkErrorModel)) return;
411
+
412
+ const frameworkErrorKey = `${$.get(frameworkErrorModel).kind}|${$.get(frameworkErrorModel).source}|${$.get(frameworkErrorModel).message}`;
413
+
414
+ if (deliveredFrameworkErrorKey === frameworkErrorKey) return;
415
+
416
+ // Hook prop became available after the framework-error model was
417
+ // already produced (e.g. host wired it asynchronously). Re-deliver
418
+ // once for the current model so late-binding hosts see the error.
419
+ deliverFrameworkErrorHook($.get(frameworkErrorModel));
254
420
  });
255
421
 
256
422
  function hashString(input) {
@@ -392,8 +558,41 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
392
558
  throw new Error("pie-assessment-toolkit requires createSectionController when no coordinator hook provides one");
393
559
  }
394
560
 
561
+ /**
562
+ * Merge the `enabled-tools` shorthand into a `tools.placement.section`
563
+ * list, leaving the `tools` object form authoritative when both are
564
+ * provided. Mirrors `resolveToolsConfig` in the section-player layer
565
+ * so direct toolkit hosts get the same easy-tier behavior.
566
+ *
567
+ * Precedence (highest first):
568
+ * 1. Explicit `tools.placement.section` array on the `tools` prop
569
+ * 2. `enabled-tools` shorthand attribute / `enabledTools` prop
570
+ *
571
+ * Section-player CEs already merge before passing `tools` here, so for
572
+ * the embedded path this helper is a no-op in practice; it only fires
573
+ * when a host mounts `<pie-assessment-toolkit>` directly.
574
+ */
575
+ function buildEffectiveToolsInput() {
576
+ const baseTools = tools() || {};
577
+ const sectionShorthand = parseToolList(enabledTools());
578
+
579
+ if (sectionShorthand.length === 0) return baseTools;
580
+
581
+ const placement = baseTools.placement || {};
582
+ const explicitSectionPlacement = Array.isArray(placement.section) ? placement.section : null;
583
+
584
+ if (explicitSectionPlacement && explicitSectionPlacement.length > 0) {
585
+ return baseTools;
586
+ }
587
+
588
+ return {
589
+ ...baseTools,
590
+ placement: { ...placement, section: sectionShorthand }
591
+ };
592
+ }
593
+
395
594
  function validateToolsConfigForBootstrap() {
396
- return normalizeAndValidateToolsConfig(tools(), {
595
+ return normalizeAndValidateToolsConfig(buildEffectiveToolsInput(), {
397
596
  strictness: toolConfigStrictness(),
398
597
  source: "pie-assessment-toolkit.bootstrap",
399
598
  toolRegistry: toolRegistry()
@@ -404,7 +603,7 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
404
603
  let toolsSignature = "";
405
604
 
406
605
  try {
407
- toolsSignature = JSON.stringify(tools() || {});
606
+ toolsSignature = JSON.stringify(buildEffectiveToolsInput());
408
607
  } catch {
409
608
  toolsSignature = "[unserializable-tools]";
410
609
  }
@@ -416,6 +615,39 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
416
615
  ].join("|");
417
616
  }
418
617
 
618
+ // Coerce the public `qti-enforcement` CE attribute (string or null)
619
+ // into the engine's `QtiEnforcementMode | null` contract. Anything
620
+ // outside the canonical "on" / "off" set — typos like "ON",
621
+ // missing-attribute artifacts like "" or "null" strings — collapses
622
+ // to `null` (auto-mode) instead of silently degrading inside the
623
+ // engine. This is the single canonical entry point for the prop;
624
+ // keep all `setQtiEnforcement(...)` calls in this file routed
625
+ // through it.
626
+ function coerceQtiEnforcement(value) {
627
+ return value === "on" || value === "off" ? value : null;
628
+ }
629
+
630
+ // Resolve the effective override that flows into the coordinator.
631
+ // The explicit `qti-enforcement` attribute (standalone path) wins
632
+ // over `tools.qtiEnforcement` (embedded path via
633
+ // `runtime.tools.qtiEnforcement`); both fall back to `null`
634
+ // (auto-mode) so the coordinator's
635
+ // {@link resolveDefaultQtiEnforcement} helper runs on the bound
636
+ // assessment / item ref.
637
+ function resolveQtiEnforcementInput(explicit, toolsConfig) {
638
+ const explicitMode = coerceQtiEnforcement(explicit);
639
+
640
+ if (explicitMode) return explicitMode;
641
+
642
+ if (toolsConfig && typeof toolsConfig === "object") {
643
+ const candidate = toolsConfig.qtiEnforcement;
644
+
645
+ return coerceQtiEnforcement(typeof candidate === "string" ? candidate : null);
646
+ }
647
+
648
+ return null;
649
+ }
650
+
419
651
  function buildOwnedCoordinator(validatedTools) {
420
652
  const fallbackAssessmentId = assessmentId() || section()?.identifier || `assessment-${Math.random().toString(16).slice(2)}`;
421
653
 
@@ -426,7 +658,8 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
426
658
  deferToolConfigValidation: true,
427
659
  tools: validatedTools,
428
660
  toolRegistry: toolRegistry(),
429
- accessibility: accessibility()
661
+ accessibility: accessibility(),
662
+ frameworkErrorBus
430
663
  });
431
664
  }
432
665
 
@@ -438,52 +671,69 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
438
671
  return coordinator() || $.get(ownedCoordinator);
439
672
  });
440
673
 
674
+ // Owned-coordinator bootstrap. The effect must re-run when ownership
675
+ // inputs (`host`, `coordinator`, `isolation`, `inheritedRuntime`)
676
+ // change so the toolkit can swap between owned, passed-in, and
677
+ // inherited coordinators. It must *not* re-run on its own writes to
678
+ // `ownedCoordinator` / `lastOwnedBootstrapFailureKey` /
679
+ // `frameworkError*` / `runtimeError` — those self-mutations were the
680
+ // observed source of the `effect_update_depth_exceeded` warnings in
681
+ // the assessment-player smoke flow. We therefore explicitly track
682
+ // only the ownership inputs and run the bootstrap body inside
683
+ // `untrack`, matching `.cursor/rules/svelte-subscription-safety.mdc`.
441
684
  $.user_effect(() => {
442
- if (!$.get(host)) return;
685
+ void $.get(host);
686
+ void coordinator();
687
+ void isolation();
688
+ void $.get(inheritedRuntime);
443
689
 
444
- if (coordinator()) {
445
- if ($.get(ownedCoordinator)) {
446
- $.set(ownedCoordinator, null);
447
- }
690
+ untrack(() => {
691
+ if (!$.get(host)) return;
448
692
 
449
- return;
450
- }
693
+ if (coordinator()) {
694
+ if ($.get(ownedCoordinator)) {
695
+ $.set(ownedCoordinator, null);
696
+ }
451
697
 
452
- if (isolation() !== "force" && $.get(inheritedRuntime)?.coordinator) {
453
- if ($.get(ownedCoordinator)) {
454
- $.set(ownedCoordinator, null);
698
+ return;
455
699
  }
456
700
 
457
- return;
458
- }
459
-
460
- if (!$.get(ownedCoordinator)) {
461
- const failureKey = getOwnedBootstrapFailureKey();
701
+ if (isolation() !== "force" && $.get(inheritedRuntime)?.coordinator) {
702
+ if ($.get(ownedCoordinator)) {
703
+ $.set(ownedCoordinator, null);
704
+ }
462
705
 
463
- if ($.get(lastOwnedBootstrapFailureKey) === failureKey) {
464
706
  return;
465
707
  }
466
708
 
467
- try {
468
- const validatedTools = validateToolsConfigForBootstrap();
469
-
470
- $.set(ownedCoordinator, buildOwnedCoordinator(validatedTools), true);
471
- $.set(lastOwnedBootstrapFailureKey, "");
472
- $.set(frameworkErrorModel, null);
473
- $.set(frameworkErrorTitle, "Unable to initialize assessment toolkit.");
474
- $.set(frameworkErrorDetails, [], true);
475
- } catch(error) {
476
- $.set(runtimeError, error, true);
477
- $.set(ownedCoordinator, null);
478
- $.set(lastOwnedBootstrapFailureKey, failureKey, true);
709
+ if (!$.get(ownedCoordinator)) {
710
+ const failureKey = getOwnedBootstrapFailureKey();
479
711
 
480
- reportFrameworkError({
481
- kind: "coordinator-init",
482
- source: "pie-assessment-toolkit",
483
- error
484
- });
712
+ if (lastOwnedBootstrapFailureKey === failureKey) {
713
+ return;
714
+ }
715
+
716
+ try {
717
+ const validatedTools = validateToolsConfigForBootstrap();
718
+
719
+ $.set(ownedCoordinator, buildOwnedCoordinator(validatedTools), true);
720
+ lastOwnedBootstrapFailureKey = "";
721
+ $.set(frameworkErrorModel, null);
722
+ $.set(frameworkErrorTitle, "Unable to initialize assessment toolkit.");
723
+ $.set(frameworkErrorDetails, [], true);
724
+ } catch(error) {
725
+ $.set(runtimeError, error, true);
726
+ $.set(ownedCoordinator, null);
727
+ lastOwnedBootstrapFailureKey = failureKey;
728
+
729
+ reportFrameworkError({
730
+ kind: "coordinator-init",
731
+ source: "pie-assessment-toolkit",
732
+ error
733
+ });
734
+ }
485
735
  }
486
- }
736
+ });
487
737
  });
488
738
 
489
739
  const effectiveAssessmentId = $.derived(() => assessmentId() || $.get(effectiveCoordinator)?.assessmentId || "");
@@ -626,12 +876,33 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
626
876
  });
627
877
  });
628
878
 
879
+ // M7 PR 6: cross-CE engine bridge consumer. When the toolkit CE
880
+ // renders inside a section-player layout, the kernel publishes its
881
+ // `SectionRuntimeEngine` via `sectionRuntimeEngineHostContext` on
882
+ // the layout CE host. The consumer's `context-request` bubbles up
883
+ // across the toolkit's shadow boundary, the kernel's provider
884
+ // answers, and we record the upstream reference. When standalone,
885
+ // no provider responds within the consumer's retry window and
886
+ // `upstreamEngine` stays `null` — that is the standalone path's
887
+ // contract. `isolation === "force"` does not opt out of this
888
+ // bridge: it is a runtime-engine seam, not a coordinator-isolation
889
+ // seam, and `force` still wants the canonical engine path.
890
+ $.user_effect(() => {
891
+ if (!$.get(host)) return;
892
+
893
+ return connectSectionRuntimeEngineHostContext($.get(host), (value) => {
894
+ if (value.engine === $.get(upstreamEngine)) return;
895
+
896
+ $.set(upstreamEngine, value.engine, true);
897
+ });
898
+ });
899
+
629
900
  $.user_effect(() => {
630
901
  const parentRuntimeId = isolation() !== "force" && $.get(inheritedRuntime) ? $.get(inheritedRuntime).runtimeId : null;
631
902
  const ownership = parentRuntimeId ? "inherited" : "owned";
632
903
 
633
- if (ownership !== $.get(lastOwnership)) {
634
- $.set(lastOwnership, ownership, true);
904
+ if (ownership !== lastOwnership) {
905
+ lastOwnership = ownership;
635
906
  emit(ownership === "inherited" ? "runtime-inherited" : "runtime-owned", { runtimeId, parentRuntimeId });
636
907
  }
637
908
  });
@@ -723,6 +994,62 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
723
994
  });
724
995
  });
725
996
 
997
+ // M8 — push Tool Policy Engine inputs (`assessment`,
998
+ // `currentItemRef`, `qtiEnforcement`) into the toolkit-owned
999
+ // coordinator whenever they change. The coordinator's
1000
+ // `updateAssessment` / `updateCurrentItemRef` / `setQtiEnforcement`
1001
+ // forwards land on `ToolPolicyEngine.updateInputs`, which value-
1002
+ // diffs each key with `Object.is` and only emits an `inputs` event
1003
+ // on real changes — so re-running this effect on coordinator swap
1004
+ // is safe and self-idempotent. We touch the reactive props
1005
+ // explicitly and then run the side effect inside `untrack(...)`
1006
+ // per `.cursor/rules/svelte-subscription-safety.mdc`.
1007
+ //
1008
+ // `qtiEnforcement` resolves through {@link resolveQtiEnforcementInput}
1009
+ // so the embedded path (`<pie-section-player-*>` setting
1010
+ // `runtime.tools.qtiEnforcement`) and the standalone path (the
1011
+ // explicit `qti-enforcement` attribute on `<pie-assessment-toolkit>`)
1012
+ // converge on a single coordinator call. The explicit prop wins
1013
+ // over `tools.qtiEnforcement` so a host can pin a stricter mode
1014
+ // without rewriting the runtime overlay.
1015
+ //
1016
+ // CRITICAL: only push when the toolkit *owns* the coordinator
1017
+ // (i.e. `effectiveCoordinator === ownedCoordinator`). When the
1018
+ // host passes a coordinator via the `coordinator` prop or shares
1019
+ // one through `assessmentToolkitHostRuntimeContext`, that
1020
+ // coordinator's policy inputs are the host's contract — overwriting
1021
+ // them with our prop defaults would silently null out a host's
1022
+ // pre-bound `AssessmentEntity`, etc. Hosts that share a coordinator
1023
+ // drive policy inputs directly via `coord.updateAssessment(...)`.
1024
+ $.user_effect(() => {
1025
+ void assessment();
1026
+ void currentItemRef();
1027
+ void qtiEnforcement();
1028
+ void tools();
1029
+
1030
+ const coord = $.get(effectiveCoordinator);
1031
+
1032
+ if (!coord) return;
1033
+
1034
+ // Host-owned coordinators are off-limits for this effect.
1035
+ if (coord !== $.get(ownedCoordinator)) return;
1036
+
1037
+ untrack(() => {
1038
+ // Apply order matters: `setQtiEnforcement` lands the override
1039
+ // (or clears it) FIRST so `updateAssessment(...)`'s
1040
+ // auto-promote path sees the final override and doesn't
1041
+ // briefly resolve to "on" (when binding an assessment with
1042
+ // `qtiEnforcement="off"`) or "off" (when clearing an
1043
+ // assessment with `qtiEnforcement="on"`). Without this
1044
+ // ordering we would emit a transient wrong-state policy
1045
+ // event between the calls.
1046
+ coord.setQtiEnforcement(resolveQtiEnforcementInput(qtiEnforcement(), tools()));
1047
+
1048
+ coord.updateAssessment(assessment());
1049
+ coord.updateCurrentItemRef(currentItemRef());
1050
+ });
1051
+ });
1052
+
726
1053
  $.user_effect(() => {
727
1054
  if (!$.get(effectiveCoordinator)) return;
728
1055
 
@@ -730,7 +1057,11 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
730
1057
  if (!isInstrumentationProvider($.get(instrumentationProvider))) return;
731
1058
  if (!$.get(instrumentationProvider).isReady()) return;
732
1059
 
733
- const instrumentationEventName = eventName.startsWith("pie-") ? eventName : `pie-toolkit-${eventName}`;
1060
+ // Telemetry event names are prefixed at the emit site in
1061
+ // `ToolkitCoordinator.emitTelemetry`. See the JSDoc on that
1062
+ // method for the namespace convention. No fallback here.
1063
+ const instrumentationEventName = eventName;
1064
+
734
1065
  const timestamp = new Date().toISOString();
735
1066
 
736
1067
  const attributes = {
@@ -792,7 +1123,35 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
792
1123
  });
793
1124
 
794
1125
  emit("section-ready", { sectionId: $.get(effectiveSectionId) });
1126
+
1127
+ // Mark section initialization complete. The reactive
1128
+ // effect below joins this with `engineReadyStageEntered`
1129
+ // so `interactive` only fires once the canonical
1130
+ // predecessor is in. Direct emit here would race with
1131
+ // `waitUntilReady()` and silently auto-skip
1132
+ // `engine-ready` on fast hydration paths.
1133
+ untrack(() => {
1134
+ $.set(sectionInitialized, true);
1135
+ });
795
1136
  }).catch((error) => {
1137
+ untrack(() => {
1138
+ // If `engine-ready` has not latched yet, mark it
1139
+ // explicitly skipped so subscribers see a monotonic
1140
+ // chain (`composed → engine-ready:skipped →
1141
+ // interactive:failed`) instead of `engine-ready`
1142
+ // silently disappearing from the canonical stream.
1143
+ // `waitUntilReady()` is still in flight at this
1144
+ // point; `engineReadyStageEntered` guards its
1145
+ // resolver so a late success cannot regress past
1146
+ // the recorded skip.
1147
+ if (!$.get(engineReadyStageEntered)) {
1148
+ $.set(engineReadyStageEntered, true);
1149
+ stageTracker.enter("engine-ready", "skipped");
1150
+ }
1151
+
1152
+ stageTracker.enter("interactive", "failed");
1153
+ });
1154
+
796
1155
  $.set(runtimeError, error, true);
797
1156
  sectionEngine.reportSectionError({ source: "section-runtime", error, timestamp: Date.now() });
798
1157
 
@@ -955,6 +1314,122 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
955
1314
  await sectionEngine.hydrate();
956
1315
  }
957
1316
 
1317
+ // Stage: `disposed` — wired on CE unmount. The four-stage retro
1318
+ // dropped `attached` (it had no consumers); the toolkit no longer
1319
+ // emits anything on mount and starts the canonical chain at
1320
+ // `composed` once the section composition lands. `disposed` still
1321
+ // fires on CE teardown so subscribers see a monotonic close-out.
1322
+ $.user_effect(() => {
1323
+ return () => {
1324
+ untrack(() => {
1325
+ if (stageTracker.getCurrent() !== null) {
1326
+ stageTracker.enter("disposed");
1327
+ }
1328
+ });
1329
+ };
1330
+ });
1331
+
1332
+ // Cohort change handler: when `(sectionId, attemptId)` changes, emit
1333
+ // `disposed` for the outgoing cohort and reset the tracker so the
1334
+ // new cohort begins emitting from `composed` against the same DOM
1335
+ // element. The boolean stage flags must reset in lockstep.
1336
+ $.user_effect(() => {
1337
+ const nextKey = `${sectionId()}|${attemptId()}`;
1338
+
1339
+ untrack(() => {
1340
+ if (nextKey === $.get(lastStageCohortKey)) return;
1341
+
1342
+ if (stageTracker.getCurrent() !== null) {
1343
+ stageTracker.enter("disposed");
1344
+ }
1345
+
1346
+ stageTracker.reset({
1347
+ sectionId: sectionId() || undefined,
1348
+ attemptId: attemptId() || undefined
1349
+ });
1350
+
1351
+ $.set(lastStageCohortKey, nextKey);
1352
+ $.set(composedStageEntered, false);
1353
+ $.set(engineReadyStageEntered, false);
1354
+ $.set(interactiveStageEntered, false);
1355
+ $.set(sectionInitialized, false);
1356
+ });
1357
+ });
1358
+
1359
+ // `interactive` fires once both canonical predecessors land:
1360
+ // `engine-ready` (coordinator bring-up settled) and
1361
+ // `sectionInitialized` (section composition + controller hydrated).
1362
+ // Joining here prevents the race that would otherwise silently
1363
+ // auto-skip `engine-ready` when the section initializer wins.
1364
+ $.user_effect(() => {
1365
+ const ready = $.get(engineReadyStageEntered) && $.get(sectionInitialized);
1366
+
1367
+ untrack(() => {
1368
+ if (!ready) return;
1369
+ if ($.get(interactiveStageEntered)) return;
1370
+
1371
+ $.set(interactiveStageEntered, true);
1372
+ stageTracker.enter("interactive");
1373
+ });
1374
+ });
1375
+
1376
+ // Canonical M6 stage progression for the toolkit CE (post-retro:
1377
+ // 4 stages). The tracker is idempotent and rejects backward
1378
+ // transitions; gating with `composedStageEntered` keeps the emit
1379
+ // order monotonic across asynchronous sources.
1380
+ $.user_effect(() => {
1381
+ const composed = $.get(compositionVersion) > 0 || $.get(compositionModel) !== null;
1382
+
1383
+ untrack(() => {
1384
+ if (!$.get(composedStageEntered) && composed) {
1385
+ $.set(composedStageEntered, true);
1386
+ stageTracker.enter("composed");
1387
+ }
1388
+ });
1389
+ });
1390
+
1391
+ // `engine-ready` resolves once `effectiveCoordinator.waitUntilReady()`
1392
+ // settles — that promise covers state load, TTS bring-up, and provider
1393
+ // initialization. Failures still flow through the framework-error bus;
1394
+ // here we record the position with `failed` so subscribers see a
1395
+ // monotonic stage chain even when the coordinator throws during
1396
+ // readiness. Gated on `composedStageEntered` so the engine-ready
1397
+ // emission cannot precede `composed` even if the coordinator
1398
+ // resolves before composition lands (rare but possible during
1399
+ // fast-hydration paths).
1400
+ $.user_effect(() => {
1401
+ if (!$.get(effectiveCoordinator)) return;
1402
+ if (!$.get(composedStageEntered)) return;
1403
+ if ($.get(engineReadyStageEntered)) return;
1404
+
1405
+ const coord = $.get(effectiveCoordinator);
1406
+ let cancelled = false;
1407
+
1408
+ void coord.waitUntilReady().then(() => {
1409
+ if (cancelled) return;
1410
+
1411
+ untrack(() => {
1412
+ if ($.get(engineReadyStageEntered)) return;
1413
+
1414
+ $.set(engineReadyStageEntered, true);
1415
+ stageTracker.enter("engine-ready");
1416
+ });
1417
+ }).catch(() => {
1418
+ if (cancelled) return;
1419
+
1420
+ untrack(() => {
1421
+ if ($.get(engineReadyStageEntered)) return;
1422
+
1423
+ $.set(engineReadyStageEntered, true);
1424
+ stageTracker.enter("engine-ready", "failed");
1425
+ });
1426
+ });
1427
+
1428
+ return () => {
1429
+ cancelled = true;
1430
+ };
1431
+ });
1432
+
958
1433
  $.user_effect(() => {
959
1434
  return () => {
960
1435
  if (compositionEmitRaf !== null && typeof window !== "undefined") {
@@ -973,6 +1448,8 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
973
1448
  error,
974
1449
  recoverable: true
975
1450
  });
1451
+ }).finally(() => {
1452
+ frameworkErrorBus.dispose();
976
1453
  });
977
1454
  };
978
1455
  });
@@ -1022,15 +1499,6 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
1022
1499
  $.flush();
1023
1500
  },
1024
1501
 
1025
- get view() {
1026
- return view();
1027
- },
1028
-
1029
- set view($$value) {
1030
- view($$value);
1031
- $.flush();
1032
- },
1033
-
1034
1502
  get env() {
1035
1503
  return env();
1036
1504
  },
@@ -1067,6 +1535,15 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
1067
1535
  $.flush();
1068
1536
  },
1069
1537
 
1538
+ get enabledTools() {
1539
+ return enabledTools();
1540
+ },
1541
+
1542
+ set enabledTools($$value) {
1543
+ enabledTools($$value);
1544
+ $.flush();
1545
+ },
1546
+
1070
1547
  get toolRegistry() {
1071
1548
  return toolRegistry();
1072
1549
  },
@@ -1121,39 +1598,57 @@ export default function PieAssessmentToolkit($$anchor, $$props) {
1121
1598
  $.flush();
1122
1599
  },
1123
1600
 
1124
- get frameworkErrorHook() {
1125
- return frameworkErrorCallback();
1601
+ get onFrameworkError() {
1602
+ return onFrameworkError();
1603
+ },
1604
+
1605
+ set onFrameworkError($$value) {
1606
+ onFrameworkError($$value);
1607
+ $.flush();
1608
+ },
1609
+
1610
+ get errorRenderer() {
1611
+ return errorRenderer();
1126
1612
  },
1127
1613
 
1128
- set frameworkErrorHook($$value) {
1129
- frameworkErrorCallback($$value);
1614
+ set errorRenderer($$value) {
1615
+ errorRenderer($$value);
1130
1616
  $.flush();
1131
1617
  },
1132
1618
 
1133
- get onFrameworkError() {
1134
- return onFrameworkError();
1619
+ get onStageChange() {
1620
+ return onStageChange();
1135
1621
  },
1136
1622
 
1137
- set onFrameworkError($$value) {
1138
- onFrameworkError($$value);
1623
+ set onStageChange($$value) {
1624
+ onStageChange($$value);
1139
1625
  $.flush();
1140
1626
  },
1141
1627
 
1142
- get onframeworkerror() {
1143
- return onframeworkerror();
1628
+ get assessment() {
1629
+ return assessment();
1144
1630
  },
1145
1631
 
1146
- set onframeworkerror($$value) {
1147
- onframeworkerror($$value);
1632
+ set assessment($$value) {
1633
+ assessment($$value);
1148
1634
  $.flush();
1149
1635
  },
1150
1636
 
1151
- get errorRenderer() {
1152
- return errorRenderer();
1637
+ get currentItemRef() {
1638
+ return currentItemRef();
1153
1639
  },
1154
1640
 
1155
- set errorRenderer($$value) {
1156
- errorRenderer($$value);
1641
+ set currentItemRef($$value) {
1642
+ currentItemRef($$value);
1643
+ $.flush();
1644
+ },
1645
+
1646
+ get qtiEnforcement() {
1647
+ return qtiEnforcement();
1648
+ },
1649
+
1650
+ set qtiEnforcement($$value) {
1651
+ qtiEnforcement($$value);
1157
1652
  $.flush();
1158
1653
  },
1159
1654
 
@@ -1230,22 +1725,24 @@ __pieDefineSafely('pie-assessment-toolkit', $.create_custom_element(
1230
1725
  section: { attribute: 'section', type: 'Object' },
1231
1726
  sectionId: { attribute: 'section-id', type: 'String' },
1232
1727
  attemptId: { attribute: 'attempt-id', type: 'String' },
1233
- view: { attribute: 'view', type: 'String' },
1234
1728
  env: { attribute: 'env', type: 'Object' },
1235
1729
  lazyInit: { attribute: 'lazy-init', type: 'Boolean' },
1236
1730
  toolConfigStrictness: { attribute: 'tool-config-strictness', type: 'String' },
1237
1731
  tools: { attribute: 'tools', type: 'Object' },
1732
+ enabledTools: { attribute: 'enabled-tools', type: 'String' },
1238
1733
  toolRegistry: { type: 'Object' },
1239
- accessibility: { attribute: 'accessibility', type: 'Object' },
1734
+ accessibility: { type: 'Object' },
1240
1735
  player: { attribute: 'player', type: 'Object' },
1241
1736
  playerType: { attribute: 'player-type', type: 'String' },
1242
1737
  coordinator: { type: 'Object' },
1243
1738
  createSectionController: { type: 'Object' },
1244
- frameworkErrorHook: { type: 'Object' },
1245
1739
  onFrameworkError: { type: 'Object' },
1246
- onframeworkerror: { type: 'Object' },
1247
1740
  errorRenderer: { type: 'Object' },
1248
- isolation: { attribute: 'isolation', type: 'String' }
1741
+ onStageChange: { type: 'Object' },
1742
+ assessment: { type: 'Object' },
1743
+ currentItemRef: { type: 'Object' },
1744
+ qtiEnforcement: { attribute: 'qti-enforcement', type: 'String' },
1745
+ isolation: { type: 'Object' }
1249
1746
  },
1250
1747
  ['default'],
1251
1748
  [