@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.
- package/README.md +289 -5
- package/dist/components/ItemToolBar.custom-element.js +165 -92
- package/dist/components/PieAssessmentToolkit.custom-element.js +604 -107
- package/dist/components/SectionToolBar.custom-element.js +0 -36
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/policy/core/PolicySource.d.ts +64 -0
- package/dist/policy/core/PolicySource.d.ts.map +1 -0
- package/dist/policy/core/PolicySource.js +17 -0
- package/dist/policy/core/PolicySource.js.map +1 -0
- package/dist/policy/core/ToolPolicyEngine.d.ts +123 -0
- package/dist/policy/core/ToolPolicyEngine.d.ts.map +1 -0
- package/dist/policy/core/ToolPolicyEngine.js +217 -0
- package/dist/policy/core/ToolPolicyEngine.js.map +1 -0
- package/dist/policy/core/compose-decision.d.ts +37 -0
- package/dist/policy/core/compose-decision.d.ts.map +1 -0
- package/dist/policy/core/compose-decision.js +332 -0
- package/dist/policy/core/compose-decision.js.map +1 -0
- package/dist/policy/core/decision-types.d.ts +142 -0
- package/dist/policy/core/decision-types.d.ts.map +1 -0
- package/dist/policy/core/decision-types.js +13 -0
- package/dist/policy/core/decision-types.js.map +1 -0
- package/dist/policy/core/engine-context.d.ts +30 -0
- package/dist/policy/core/engine-context.d.ts.map +1 -0
- package/dist/policy/core/engine-context.js +28 -0
- package/dist/policy/core/engine-context.js.map +1 -0
- package/dist/policy/core/policy-source-tag.d.ts +32 -0
- package/dist/policy/core/policy-source-tag.d.ts.map +1 -0
- package/dist/policy/core/policy-source-tag.js +29 -0
- package/dist/policy/core/policy-source-tag.js.map +1 -0
- package/dist/policy/core/provenance.d.ts +163 -0
- package/dist/policy/core/provenance.d.ts.map +1 -0
- package/dist/policy/core/provenance.js +232 -0
- package/dist/policy/core/provenance.js.map +1 -0
- package/dist/policy/core/qti-inputs.d.ts +67 -0
- package/dist/policy/core/qti-inputs.d.ts.map +1 -0
- package/dist/policy/core/qti-inputs.js +139 -0
- package/dist/policy/core/qti-inputs.js.map +1 -0
- package/dist/policy/engine.d.ts +28 -0
- package/dist/policy/engine.d.ts.map +1 -0
- package/dist/policy/engine.js +24 -0
- package/dist/policy/engine.js.map +1 -0
- package/dist/policy/internal.d.ts +26 -0
- package/dist/policy/internal.d.ts.map +1 -0
- package/dist/policy/internal.js +26 -0
- package/dist/policy/internal.js.map +1 -0
- package/dist/policy/sources/QtiPolicySource.d.ts +146 -0
- package/dist/policy/sources/QtiPolicySource.d.ts.map +1 -0
- package/dist/policy/sources/QtiPolicySource.js +270 -0
- package/dist/policy/sources/QtiPolicySource.js.map +1 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +145 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts.map +1 -1
- package/dist/runtime/SectionRuntimeEngine.js +164 -3
- package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +144 -0
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts.map +1 -0
- package/dist/runtime/adapter/SectionEngineAdapter.js +183 -0
- package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -0
- package/dist/runtime/adapter/coordinator-bridge.d.ts +93 -0
- package/dist/runtime/adapter/coordinator-bridge.d.ts.map +1 -0
- package/dist/runtime/adapter/coordinator-bridge.js +127 -0
- package/dist/runtime/adapter/coordinator-bridge.js.map +1 -0
- package/dist/runtime/adapter/dom-event-bridge.d.ts +63 -0
- package/dist/runtime/adapter/dom-event-bridge.d.ts.map +1 -0
- package/dist/runtime/adapter/dom-event-bridge.js +95 -0
- package/dist/runtime/adapter/dom-event-bridge.js.map +1 -0
- package/dist/runtime/adapter/framework-error-bridge.d.ts +37 -0
- package/dist/runtime/adapter/framework-error-bridge.d.ts.map +1 -0
- package/dist/runtime/adapter/framework-error-bridge.js +47 -0
- package/dist/runtime/adapter/framework-error-bridge.js.map +1 -0
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +32 -0
- package/dist/runtime/adapter/instrumentation-bridge.d.ts.map +1 -0
- package/dist/runtime/adapter/instrumentation-bridge.js +39 -0
- package/dist/runtime/adapter/instrumentation-bridge.js.map +1 -0
- package/dist/runtime/adapter/subscriber-fanout.d.ts +35 -0
- package/dist/runtime/adapter/subscriber-fanout.d.ts.map +1 -0
- package/dist/runtime/adapter/subscriber-fanout.js +60 -0
- package/dist/runtime/adapter/subscriber-fanout.js.map +1 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +52 -0
- package/dist/runtime/core/SectionEngineCore.d.ts.map +1 -0
- package/dist/runtime/core/SectionEngineCore.js +81 -0
- package/dist/runtime/core/SectionEngineCore.js.map +1 -0
- package/dist/runtime/core/cohort.d.ts +40 -0
- package/dist/runtime/core/cohort.d.ts.map +1 -0
- package/dist/runtime/core/cohort.js +49 -0
- package/dist/runtime/core/cohort.js.map +1 -0
- package/dist/runtime/core/engine-input.d.ts +114 -0
- package/dist/runtime/core/engine-input.d.ts.map +1 -0
- package/dist/runtime/core/engine-input.js +19 -0
- package/dist/runtime/core/engine-input.js.map +1 -0
- package/dist/runtime/core/engine-output.d.ts +62 -0
- package/dist/runtime/core/engine-output.d.ts.map +1 -0
- package/dist/runtime/core/engine-output.js +27 -0
- package/dist/runtime/core/engine-output.js.map +1 -0
- package/dist/runtime/core/engine-readiness.d.ts +49 -0
- package/dist/runtime/core/engine-readiness.d.ts.map +1 -0
- package/dist/runtime/core/engine-readiness.js +47 -0
- package/dist/runtime/core/engine-readiness.js.map +1 -0
- package/dist/runtime/core/engine-resolver.d.ts +172 -0
- package/dist/runtime/core/engine-resolver.d.ts.map +1 -0
- package/dist/runtime/core/engine-resolver.js +170 -0
- package/dist/runtime/core/engine-resolver.js.map +1 -0
- package/dist/runtime/core/engine-stage-derivation.d.ts +19 -0
- package/dist/runtime/core/engine-stage-derivation.d.ts.map +1 -0
- package/dist/runtime/core/engine-stage-derivation.js +30 -0
- package/dist/runtime/core/engine-stage-derivation.js.map +1 -0
- package/dist/runtime/core/engine-state.d.ts +83 -0
- package/dist/runtime/core/engine-state.d.ts.map +1 -0
- package/dist/runtime/core/engine-state.js +49 -0
- package/dist/runtime/core/engine-state.js.map +1 -0
- package/dist/runtime/core/engine-transition.d.ts +49 -0
- package/dist/runtime/core/engine-transition.d.ts.map +1 -0
- package/dist/runtime/core/engine-transition.js +261 -0
- package/dist/runtime/core/engine-transition.js.map +1 -0
- package/dist/runtime/engine-context.d.ts +40 -0
- package/dist/runtime/engine-context.d.ts.map +1 -0
- package/dist/runtime/engine-context.js +32 -0
- package/dist/runtime/engine-context.js.map +1 -0
- package/dist/runtime/engine.d.ts +24 -0
- package/dist/runtime/engine.d.ts.map +1 -0
- package/dist/runtime/engine.js +24 -0
- package/dist/runtime/engine.js.map +1 -0
- package/dist/runtime/internal.d.ts +44 -0
- package/dist/runtime/internal.d.ts.map +1 -0
- package/dist/runtime/internal.js +46 -0
- package/dist/runtime/internal.js.map +1 -0
- package/dist/runtime/runtime-id.d.ts +8 -0
- package/dist/runtime/runtime-id.d.ts.map +1 -0
- package/dist/runtime/runtime-id.js +10 -0
- package/dist/runtime/runtime-id.js.map +1 -0
- package/dist/runtime/section-runtime-engine-host-context.d.ts +74 -0
- package/dist/runtime/section-runtime-engine-host-context.d.ts.map +1 -0
- package/dist/runtime/section-runtime-engine-host-context.js +96 -0
- package/dist/runtime/section-runtime-engine-host-context.js.map +1 -0
- package/dist/runtime/stage-emit-gate.d.ts +93 -0
- package/dist/runtime/stage-emit-gate.d.ts.map +1 -0
- package/dist/runtime/stage-emit-gate.js +83 -0
- package/dist/runtime/stage-emit-gate.js.map +1 -0
- package/dist/services/ToolkitCoordinator.d.ts +280 -7
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.js +412 -32
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/framework-error-bus.d.ts +73 -0
- package/dist/services/framework-error-bus.d.ts.map +1 -0
- package/dist/services/framework-error-bus.js +68 -0
- package/dist/services/framework-error-bus.js.map +1 -0
- package/dist/services/framework-error.d.ts +36 -1
- package/dist/services/framework-error.d.ts.map +1 -1
- package/dist/services/framework-error.js +37 -0
- package/dist/services/framework-error.js.map +1 -1
- package/dist/services/interfaces.d.ts +78 -0
- package/dist/services/interfaces.d.ts.map +1 -1
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/section-controller-types.d.ts +20 -0
- package/dist/services/section-controller-types.d.ts.map +1 -1
- package/dist/services/tools-config-normalizer.d.ts +23 -0
- package/dist/services/tools-config-normalizer.d.ts.map +1 -1
- package/dist/services/tools-config-normalizer.js +13 -1
- package/dist/services/tools-config-normalizer.js.map +1 -1
- package/dist/tools/client.d.ts +0 -1
- package/dist/tools/client.d.ts.map +1 -1
- package/dist/tools/client.js +0 -2
- package/dist/tools/client.js.map +1 -1
- package/dist/tools/types.d.ts +0 -26
- package/dist/tools/types.d.ts.map +1 -1
- package/package.json +23 -7
- package/dist/runtime/runtime-event-guards.d.ts +0 -2
- package/dist/runtime/runtime-event-guards.d.ts.map +0 -1
- package/dist/runtime/runtime-event-guards.js +0 -4
- package/dist/runtime/runtime-event-guards.js.map +0 -1
- package/dist/tools/tool-coordinator.d.ts +0 -72
- package/dist/tools/tool-coordinator.d.ts.map +0 -1
- package/dist/tools/tool-coordinator.js +0 -194
- package/dist/tools/tool-coordinator.js.map +0 -1
- package/dist/types/events.d.ts +0 -157
- package/dist/types/events.d.ts.map +0 -1
- package/dist/types/events.js +0 -11
- package/dist/types/events.js.map +0 -1
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Part of PIE Assessment Toolkit.
|
|
15
15
|
*/
|
|
16
|
-
import { normalizeToolsConfig,
|
|
16
|
+
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
17
17
|
import { normalizeAndValidateToolsConfig, normalizeToolConfigStrictness, } from "./tool-config-validation.js";
|
|
18
18
|
import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
|
|
19
19
|
import { ElementToolStateStore } from "./ElementToolStateStore.js";
|
|
20
|
+
import { frameworkErrorFromCoordinatorContext, } from "./framework-error.js";
|
|
21
|
+
import { FrameworkErrorBus, } from "./framework-error-bus.js";
|
|
20
22
|
import { HighlightCoordinator } from "./HighlightCoordinator.js";
|
|
21
23
|
import { ToolCoordinator } from "./ToolCoordinator.js";
|
|
22
24
|
import { TTSService } from "./TTSService.js";
|
|
@@ -24,6 +26,8 @@ import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
|
24
26
|
import { buildRuntimeTTSConfig, resolveTTSBackend, } from "./tts-runtime-config.js";
|
|
25
27
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
26
28
|
import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
|
|
29
|
+
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
30
|
+
import { resolveDefaultQtiEnforcement } from "../policy/internal.js";
|
|
27
31
|
const SECTION_ITEM_EVENT_TYPES = [
|
|
28
32
|
"item-selected",
|
|
29
33
|
"item-session-data-changed",
|
|
@@ -98,7 +102,51 @@ export class ToolkitCoordinator {
|
|
|
98
102
|
sectionEventListenerIds = new WeakMap();
|
|
99
103
|
sectionEventSubscriptions = new Map();
|
|
100
104
|
telemetryListeners = new Set();
|
|
105
|
+
frameworkErrorBus;
|
|
106
|
+
ownsFrameworkErrorBus;
|
|
101
107
|
nextSectionEventListenerId = 1;
|
|
108
|
+
/**
|
|
109
|
+
* Unified Tool Policy Engine. Owned by the coordinator and lives
|
|
110
|
+
* for the lifetime of the coordinator instance — there is no
|
|
111
|
+
* explicit teardown path today; the engine and its listener set
|
|
112
|
+
* are reclaimed by GC when the coordinator becomes unreachable.
|
|
113
|
+
* Subscribers attached via {@link onPolicyChange} must therefore
|
|
114
|
+
* detach via the unsubscribe function the engine returns; do not
|
|
115
|
+
* rely on a `disposed` event being emitted on coordinator teardown.
|
|
116
|
+
*
|
|
117
|
+
* Hosts read decisions via {@link decideToolPolicy} or subscribe
|
|
118
|
+
* to changes via {@link onPolicyChange}. The legacy
|
|
119
|
+
* `resolveToolsForLevel` / `PnpToolResolver` paths still coexist
|
|
120
|
+
* until the upcoming compat-removal sweep deletes them.
|
|
121
|
+
*/
|
|
122
|
+
policyEngine;
|
|
123
|
+
/**
|
|
124
|
+
* Host-set override for QTI enforcement. `null` (the default) means
|
|
125
|
+
* "auto" — the coordinator infers the effective mode from the
|
|
126
|
+
* QTI inputs the bound `AssessmentEntity` and `AssessmentItemRef`
|
|
127
|
+
* actually carry (see {@link resolveEffectiveQtiEnforcement}).
|
|
128
|
+
* `"on"` / `"off"` are explicit host opt-in / opt-out and stick
|
|
129
|
+
* across subsequent assessment / item swaps until the host clears
|
|
130
|
+
* the override by calling `setQtiEnforcement(null)`.
|
|
131
|
+
*/
|
|
132
|
+
qtiEnforcementOverride = null;
|
|
133
|
+
/**
|
|
134
|
+
* Last assessment passed to {@link updateAssessment}. Read by
|
|
135
|
+
* {@link resolveEffectiveQtiEnforcement} to compute auto-mode.
|
|
136
|
+
* The engine's own copy is the canonical record for decisions;
|
|
137
|
+
* this mirror exists only so the auto-mode helper does not need
|
|
138
|
+
* to round-trip through {@link policyEngine}'s frozen snapshot.
|
|
139
|
+
*/
|
|
140
|
+
boundAssessment = null;
|
|
141
|
+
/**
|
|
142
|
+
* Last item reference passed to {@link updateCurrentItemRef}.
|
|
143
|
+
* Mirrored alongside {@link boundAssessment} so
|
|
144
|
+
* {@link resolveEffectiveQtiEnforcement} can detect item-level
|
|
145
|
+
* QTI inputs (`requiredTools` / `restrictedTools` /
|
|
146
|
+
* `toolParameters`) without round-tripping through the engine's
|
|
147
|
+
* frozen snapshot.
|
|
148
|
+
*/
|
|
149
|
+
boundCurrentItemRef = null;
|
|
102
150
|
/** Callback for floating tools changes */
|
|
103
151
|
floatingToolsChangeCallback = null;
|
|
104
152
|
static resolveConfig(config) {
|
|
@@ -148,6 +196,13 @@ export class ToolkitCoordinator {
|
|
|
148
196
|
this.toolRegistry = resolvedConfig.toolRegistry ?? createPackagedToolRegistry();
|
|
149
197
|
this.hooks = resolvedConfig.hooks ?? {};
|
|
150
198
|
this.lazyInit = config.lazyInit === true;
|
|
199
|
+
// Use the host-provided framework-error bus if one was passed
|
|
200
|
+
// (typical when embedded inside <pie-assessment-toolkit>, so
|
|
201
|
+
// pre-coordinator failures from the CE flow through the same
|
|
202
|
+
// fan-out as coordinator failures). Otherwise own a private one.
|
|
203
|
+
this.frameworkErrorBus = config.frameworkErrorBus ?? new FrameworkErrorBus();
|
|
204
|
+
this.ownsFrameworkErrorBus = !config.frameworkErrorBus;
|
|
205
|
+
this.subscribeFrameworkErrorHookAdapters();
|
|
151
206
|
// Initialize all services
|
|
152
207
|
this.toolCoordinator = new ToolCoordinator();
|
|
153
208
|
this.highlightCoordinator = new HighlightCoordinator();
|
|
@@ -159,6 +214,24 @@ export class ToolkitCoordinator {
|
|
|
159
214
|
// Initialize TTS service based on config
|
|
160
215
|
this.ttsService = new TTSService();
|
|
161
216
|
this.setupStatePersistenceHooks();
|
|
217
|
+
// M8 PR 2 — construct the unified ToolPolicyEngine seeded with
|
|
218
|
+
// the validated tools config. QTI inputs (`assessment`,
|
|
219
|
+
// `currentItemRef`) start `null`; `qtiEnforcement` is resolved
|
|
220
|
+
// through {@link resolveEffectiveQtiEnforcement}, which flips
|
|
221
|
+
// to `"on"` only once the bound assessment or item carries
|
|
222
|
+
// actual QTI material (PR 4). Hosts that only consume the
|
|
223
|
+
// engine for placement/policy gating get the pre-PR-2 behavior
|
|
224
|
+
// bit-for-bit.
|
|
225
|
+
this.policyEngine = new ToolPolicyEngine({
|
|
226
|
+
toolRegistry: this.toolRegistry,
|
|
227
|
+
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
228
|
+
inputs: {
|
|
229
|
+
tools: this.config.tools,
|
|
230
|
+
assessment: null,
|
|
231
|
+
currentItemRef: null,
|
|
232
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
162
235
|
if (!this.lazyInit) {
|
|
163
236
|
void this.waitUntilReady().catch((err) => {
|
|
164
237
|
console.error("[ToolkitCoordinator] Failed eager initialization:", err);
|
|
@@ -166,6 +239,50 @@ export class ToolkitCoordinator {
|
|
|
166
239
|
});
|
|
167
240
|
}
|
|
168
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Subscribe the canonical `onFrameworkError` hook adapter to the
|
|
244
|
+
* framework-error bus.
|
|
245
|
+
*
|
|
246
|
+
* Called once from the constructor. The adapter inspects the live
|
|
247
|
+
* `this.hooks` reference, so a hook added later via {@link setHooks}
|
|
248
|
+
* is picked up automatically without re-subscribing.
|
|
249
|
+
*/
|
|
250
|
+
subscribeFrameworkErrorHookAdapters() {
|
|
251
|
+
this.frameworkErrorBus.subscribeFrameworkErrors((model) => {
|
|
252
|
+
const hook = this.hooks.onFrameworkError;
|
|
253
|
+
if (!hook)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
hook(model);
|
|
257
|
+
}
|
|
258
|
+
catch (hookError) {
|
|
259
|
+
console.warn("[ToolkitCoordinator] onFrameworkError hook failed:", hookError);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Emit a telemetry event to `onTelemetry` hook + all `subscribeTelemetry`
|
|
265
|
+
* listeners.
|
|
266
|
+
*
|
|
267
|
+
* **Naming convention.** Event names MUST be prefixed at the call site,
|
|
268
|
+
* not auto-decorated here. The convention is:
|
|
269
|
+
*
|
|
270
|
+
* - `pie-toolkit-*` for toolkit lifecycle (state load, providers,
|
|
271
|
+
* coordinator readiness, section-controller register/dispose, TTS
|
|
272
|
+
* bring-up, tool-config updates, etc.).
|
|
273
|
+
* - `pie-tool-*` for individual tool events (provider init lifecycle,
|
|
274
|
+
* provider backend calls, tool-specific telemetry forwarded from
|
|
275
|
+
* `__pieTelemetry`).
|
|
276
|
+
* - `pie-section-*` is reserved for section-player layout events
|
|
277
|
+
* surfaced via `attachInstrumentationEventBridge` and is not emitted
|
|
278
|
+
* from this method directly.
|
|
279
|
+
*
|
|
280
|
+
* Anything that does not fit a documented prefix is a deliberate decision
|
|
281
|
+
* to be raised in code review, not a fallback. There is no "auto prefix"
|
|
282
|
+
* here on purpose: it keeps every emit-site honest about which namespace
|
|
283
|
+
* it owns, and lets `subscribeTelemetry` consumers compare against
|
|
284
|
+
* documented strings without per-consumer normalization.
|
|
285
|
+
*/
|
|
169
286
|
async emitTelemetry(eventName, payload) {
|
|
170
287
|
try {
|
|
171
288
|
await this.hooks.onTelemetry?.(eventName, payload);
|
|
@@ -197,13 +314,40 @@ export class ToolkitCoordinator {
|
|
|
197
314
|
}
|
|
198
315
|
}
|
|
199
316
|
handleError(error, context) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
317
|
+
const model = frameworkErrorFromCoordinatorContext({
|
|
318
|
+
error,
|
|
319
|
+
context,
|
|
320
|
+
});
|
|
321
|
+
this.frameworkErrorBus.reportFrameworkError(model);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Subscribe to framework-error events emitted by this coordinator.
|
|
325
|
+
*
|
|
326
|
+
* See `ToolkitCoordinatorApi.subscribeFrameworkErrors` for the
|
|
327
|
+
* contract. The bus is shared with the canonical
|
|
328
|
+
* `onFrameworkError` lifecycle hook, so a listener registered here
|
|
329
|
+
* sees the same fan-out the hook sees.
|
|
330
|
+
*/
|
|
331
|
+
subscribeFrameworkErrors(listener) {
|
|
332
|
+
return this.frameworkErrorBus.subscribeFrameworkErrors(listener);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Report a framework-error model directly into this coordinator's bus.
|
|
336
|
+
*
|
|
337
|
+
* Used by embedding hosts (e.g. `<pie-assessment-toolkit>`) that
|
|
338
|
+
* pre-construct their own framework-error model in a path that does
|
|
339
|
+
* not go through `handleError` — for example, `runtime-init` failures
|
|
340
|
+
* raised before any coordinator phase, or `tool-config` failures
|
|
341
|
+
* synthesized from validation diagnostics.
|
|
342
|
+
*
|
|
343
|
+
* Hosts that already share their bus via the constructor's
|
|
344
|
+
* `frameworkErrorBus` config field do not need to call this; their
|
|
345
|
+
* own `bus.reportFrameworkError(model)` is observed here.
|
|
346
|
+
*
|
|
347
|
+
* @internal
|
|
348
|
+
*/
|
|
349
|
+
reportFrameworkError(model) {
|
|
350
|
+
this.frameworkErrorBus.reportFrameworkError(model);
|
|
207
351
|
}
|
|
208
352
|
getSectionControllerMapKey(key) {
|
|
209
353
|
return `${key.assessmentId}::${key.sectionId}::${key.attemptId || ""}`;
|
|
@@ -287,7 +431,7 @@ export class ToolkitCoordinator {
|
|
|
287
431
|
this.elementToolStateStore.loadState(state);
|
|
288
432
|
}
|
|
289
433
|
this.stateLoaded = true;
|
|
290
|
-
await this.emitTelemetry("tool-state-loaded", {
|
|
434
|
+
await this.emitTelemetry("pie-toolkit-tool-state-loaded", {
|
|
291
435
|
hasState: Boolean(state),
|
|
292
436
|
});
|
|
293
437
|
}
|
|
@@ -382,7 +526,7 @@ export class ToolkitCoordinator {
|
|
|
382
526
|
providerName: config.provider.providerName,
|
|
383
527
|
};
|
|
384
528
|
await this.hooks.onProviderRegistered?.(providerId, meta);
|
|
385
|
-
await this.emitTelemetry("provider-registered", {
|
|
529
|
+
await this.emitTelemetry("pie-toolkit-provider-registered", {
|
|
386
530
|
providerId,
|
|
387
531
|
providerName: config.provider.providerName,
|
|
388
532
|
});
|
|
@@ -390,10 +534,6 @@ export class ToolkitCoordinator {
|
|
|
390
534
|
catch (err) {
|
|
391
535
|
console.warn(`[ToolkitCoordinator] Failed to register provider "${providerId}":`, err);
|
|
392
536
|
this.handleError(err, { phase: "provider-register", providerId });
|
|
393
|
-
this.hooks.onProviderError?.(providerId, err, {
|
|
394
|
-
phase: "provider-register",
|
|
395
|
-
providerId,
|
|
396
|
-
});
|
|
397
537
|
}
|
|
398
538
|
}
|
|
399
539
|
async ensureProviderReady(providerId) {
|
|
@@ -410,15 +550,11 @@ export class ToolkitCoordinator {
|
|
|
410
550
|
await this.hooks.onProviderInitStart?.(providerId, meta);
|
|
411
551
|
await this.toolProviderRegistry.initialize(providerId);
|
|
412
552
|
await this.hooks.onProviderReady?.(providerId, meta);
|
|
413
|
-
await this.emitTelemetry("provider-ready", { providerId });
|
|
553
|
+
await this.emitTelemetry("pie-toolkit-provider-ready", { providerId });
|
|
414
554
|
return provider;
|
|
415
555
|
}
|
|
416
556
|
catch (err) {
|
|
417
557
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
418
|
-
this.hooks.onProviderError?.(providerId, error, {
|
|
419
|
-
phase: "provider-init",
|
|
420
|
-
providerId,
|
|
421
|
-
});
|
|
422
558
|
this.handleError(error, { phase: "provider-init", providerId });
|
|
423
559
|
throw error;
|
|
424
560
|
}
|
|
@@ -473,6 +609,21 @@ export class ToolkitCoordinator {
|
|
|
473
609
|
return;
|
|
474
610
|
args.listener(event);
|
|
475
611
|
});
|
|
612
|
+
// Replay `content-loaded` events first so a late subscriber observes
|
|
613
|
+
// the same ordering a live subscriber would have seen (per-renderable
|
|
614
|
+
// loads, then the aggregate `section-loading-complete`). Symmetric
|
|
615
|
+
// with `section-loading-complete` replay below; without this, a
|
|
616
|
+
// consumer that attaches after a cohort transition (e.g. a fresh
|
|
617
|
+
// section controller created on navigation) silently misses every
|
|
618
|
+
// `content-loaded` event for items that finished loading before the
|
|
619
|
+
// subscription attached. See PIE-512 for the consumer-observable
|
|
620
|
+
// regression.
|
|
621
|
+
const contentLoadedReplays = this.buildContentLoadedReplayEvents(controller);
|
|
622
|
+
for (const event of contentLoadedReplays) {
|
|
623
|
+
if (shouldDeliverEvent(event)) {
|
|
624
|
+
args.listener(event);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
476
627
|
const replayEvent = this.buildLoadingCompleteReplayEvent(controller);
|
|
477
628
|
if (replayEvent && shouldDeliverEvent(replayEvent)) {
|
|
478
629
|
args.listener(replayEvent);
|
|
@@ -543,6 +694,53 @@ export class ToolkitCoordinator {
|
|
|
543
694
|
}
|
|
544
695
|
return itemIds;
|
|
545
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Build replay `content-loaded` events for renderables the controller has
|
|
699
|
+
* already finished loading.
|
|
700
|
+
*
|
|
701
|
+
* Strict by design: only renderables explicitly reported in
|
|
702
|
+
* `runtimeState.loadedRenderables` are replayed. Synthetic test harnesses
|
|
703
|
+
* that omit the field (or controllers from older revisions that did not
|
|
704
|
+
* populate it) get an empty replay set rather than a fabricated one — that
|
|
705
|
+
* preserves the contract that replays mirror events that actually fired.
|
|
706
|
+
*
|
|
707
|
+
* Order follows registration order from
|
|
708
|
+
* `SectionController.collectLoadedRenderableSnapshot`, which mirrors the
|
|
709
|
+
* order live `content-loaded` events were emitted in.
|
|
710
|
+
*/
|
|
711
|
+
buildContentLoadedReplayEvents(controller) {
|
|
712
|
+
const runtimeState = controller.getRuntimeState?.();
|
|
713
|
+
const loadedRenderables = runtimeState?.loadedRenderables;
|
|
714
|
+
if (!Array.isArray(loadedRenderables) || loadedRenderables.length === 0) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const currentItemIndex = typeof runtimeState?.currentItemIndex === "number" &&
|
|
718
|
+
Number.isFinite(runtimeState.currentItemIndex)
|
|
719
|
+
? runtimeState.currentItemIndex
|
|
720
|
+
: 0;
|
|
721
|
+
const timestamp = Date.now();
|
|
722
|
+
const events = [];
|
|
723
|
+
for (const renderable of loadedRenderables) {
|
|
724
|
+
if (!renderable)
|
|
725
|
+
continue;
|
|
726
|
+
const itemId = renderable.itemId;
|
|
727
|
+
const canonicalItemId = typeof renderable.canonicalItemId === "string" &&
|
|
728
|
+
renderable.canonicalItemId
|
|
729
|
+
? renderable.canonicalItemId
|
|
730
|
+
: itemId;
|
|
731
|
+
if (typeof itemId !== "string" || !itemId)
|
|
732
|
+
continue;
|
|
733
|
+
events.push({
|
|
734
|
+
type: "content-loaded",
|
|
735
|
+
contentKind: renderable.contentKind ?? "unknown",
|
|
736
|
+
itemId,
|
|
737
|
+
canonicalItemId,
|
|
738
|
+
currentItemIndex,
|
|
739
|
+
timestamp,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return events;
|
|
743
|
+
}
|
|
546
744
|
buildLoadingCompleteReplayEvent(controller) {
|
|
547
745
|
const runtimeState = controller.getRuntimeState?.();
|
|
548
746
|
if (runtimeState?.loadingComplete !== true)
|
|
@@ -751,7 +949,7 @@ export class ToolkitCoordinator {
|
|
|
751
949
|
controller: args.controller,
|
|
752
950
|
});
|
|
753
951
|
await this.hooks.onSectionControllerReady?.(args.context, args.controller);
|
|
754
|
-
await this.emitTelemetry("section-controller-ready", {
|
|
952
|
+
await this.emitTelemetry("pie-toolkit-section-controller-ready", {
|
|
755
953
|
assessmentId: args.key.assessmentId,
|
|
756
954
|
sectionId: args.key.sectionId,
|
|
757
955
|
attemptId: args.key.attemptId,
|
|
@@ -821,7 +1019,7 @@ export class ToolkitCoordinator {
|
|
|
821
1019
|
}
|
|
822
1020
|
await args.controller.dispose?.();
|
|
823
1021
|
await this.hooks.onSectionControllerDispose?.(args.context, args.controller);
|
|
824
|
-
await this.emitTelemetry("section-controller-disposed", {
|
|
1022
|
+
await this.emitTelemetry("pie-toolkit-section-controller-disposed", {
|
|
825
1023
|
assessmentId: args.key.assessmentId,
|
|
826
1024
|
sectionId: args.key.sectionId,
|
|
827
1025
|
attemptId: args.key.attemptId,
|
|
@@ -873,7 +1071,7 @@ export class ToolkitCoordinator {
|
|
|
873
1071
|
backend: resolvedBackend,
|
|
874
1072
|
},
|
|
875
1073
|
});
|
|
876
|
-
await this.emitTelemetry("tts-init-start", {
|
|
1074
|
+
await this.emitTelemetry("pie-toolkit-tts-init-start", {
|
|
877
1075
|
backend: resolvedBackend,
|
|
878
1076
|
});
|
|
879
1077
|
await this.emitTelemetry("pie-tool-init-start", {
|
|
@@ -887,7 +1085,7 @@ export class ToolkitCoordinator {
|
|
|
887
1085
|
const ttsProvider = await this.ensureProviderReady("tts");
|
|
888
1086
|
const providerInstance = await ttsProvider.createInstance();
|
|
889
1087
|
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
890
|
-
await this.emitTelemetry("tts-init-success", {
|
|
1088
|
+
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
891
1089
|
provider: "registry",
|
|
892
1090
|
});
|
|
893
1091
|
await this.emitTelemetry("pie-tool-init-success", {
|
|
@@ -926,7 +1124,7 @@ export class ToolkitCoordinator {
|
|
|
926
1124
|
const provider = new BrowserTTSProvider();
|
|
927
1125
|
try {
|
|
928
1126
|
await this.initializeTTSService(provider, runtimeTTSConfig);
|
|
929
|
-
await this.emitTelemetry("tts-init-success", {
|
|
1127
|
+
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
930
1128
|
provider: "browser-fallback",
|
|
931
1129
|
});
|
|
932
1130
|
await this.emitTelemetry("pie-tool-init-success", {
|
|
@@ -938,9 +1136,8 @@ export class ToolkitCoordinator {
|
|
|
938
1136
|
}
|
|
939
1137
|
catch (error) {
|
|
940
1138
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
941
|
-
this.hooks.onTTSError?.(normalized, { phase: "tts-init" });
|
|
942
1139
|
this.handleError(normalized, { phase: "tts-init" });
|
|
943
|
-
await this.emitTelemetry("tts-init-error", {
|
|
1140
|
+
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
944
1141
|
message: normalized.message,
|
|
945
1142
|
});
|
|
946
1143
|
await this.emitTelemetry("pie-tool-init-error", {
|
|
@@ -1013,7 +1210,7 @@ export class ToolkitCoordinator {
|
|
|
1013
1210
|
}
|
|
1014
1211
|
});
|
|
1015
1212
|
const voicesAfterWait = synth.getVoices();
|
|
1016
|
-
await this.emitTelemetry("tts-browser-voices-ready", {
|
|
1213
|
+
await this.emitTelemetry("pie-toolkit-tts-browser-voices-ready", {
|
|
1017
1214
|
voiceCount: voicesAfterWait.length,
|
|
1018
1215
|
timedOut: voicesAfterWait.length === 0,
|
|
1019
1216
|
});
|
|
@@ -1055,7 +1252,7 @@ export class ToolkitCoordinator {
|
|
|
1055
1252
|
if (!this.coordinatorReadyNotified) {
|
|
1056
1253
|
this.coordinatorReadyNotified = true;
|
|
1057
1254
|
await this.hooks.onCoordinatorReady?.(this);
|
|
1058
|
-
await this.emitTelemetry("coordinator-ready", {
|
|
1255
|
+
await this.emitTelemetry("pie-toolkit-coordinator-ready", {
|
|
1059
1256
|
assessmentId: this.assessmentId,
|
|
1060
1257
|
});
|
|
1061
1258
|
}
|
|
@@ -1155,7 +1352,14 @@ export class ToolkitCoordinator {
|
|
|
1155
1352
|
toolRegistry: this.toolRegistry,
|
|
1156
1353
|
});
|
|
1157
1354
|
this.config.tools = validated.config;
|
|
1158
|
-
|
|
1355
|
+
// M8 PR 2 — keep the policy engine's tools input in lockstep
|
|
1356
|
+
// with the validated coordinator config. The engine emits an
|
|
1357
|
+
// `inputs` change event so subscribers (e.g. PR 3 toolbars) can
|
|
1358
|
+
// re-decide without us managing a parallel pub/sub.
|
|
1359
|
+
this.policyEngine.updateInputs({
|
|
1360
|
+
tools: this.config.tools,
|
|
1361
|
+
});
|
|
1362
|
+
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
1159
1363
|
// Apply configuration changes to services
|
|
1160
1364
|
this._applyToolConfigChange(toolId, updates);
|
|
1161
1365
|
}
|
|
@@ -1186,20 +1390,57 @@ export class ToolkitCoordinator {
|
|
|
1186
1390
|
toolRegistry: this.toolRegistry,
|
|
1187
1391
|
});
|
|
1188
1392
|
this.config.tools = validated.config;
|
|
1393
|
+
// M8 PR 2 — keep the policy engine in lockstep with the
|
|
1394
|
+
// floating-tools placement update. See `updateToolConfig` for
|
|
1395
|
+
// the same pattern.
|
|
1396
|
+
this.policyEngine.updateInputs({
|
|
1397
|
+
tools: this.config.tools,
|
|
1398
|
+
});
|
|
1189
1399
|
// Notify listener of change
|
|
1190
1400
|
if (this.floatingToolsChangeCallback) {
|
|
1191
1401
|
this.floatingToolsChangeCallback(toolIds);
|
|
1192
1402
|
}
|
|
1193
1403
|
}
|
|
1194
1404
|
/**
|
|
1195
|
-
* Get currently enabled floating tools.
|
|
1405
|
+
* Get currently enabled floating tools (section-level placement).
|
|
1406
|
+
*
|
|
1407
|
+
* @returns Array of enabled tool IDs.
|
|
1408
|
+
*
|
|
1409
|
+
* @remarks
|
|
1410
|
+
* As of M8 PR 2 this routes through the {@link ToolPolicyEngine}
|
|
1411
|
+
* rather than the legacy `resolveToolsForLevel(...)` shim. The two
|
|
1412
|
+
* paths agree on `placement → policy.allowed → policy.blocked`,
|
|
1413
|
+
* but the engine additionally enforces:
|
|
1414
|
+
*
|
|
1415
|
+
* - **Provider veto** — `tools.providers[id].enabled === false`
|
|
1416
|
+
* removes the tool from the visible set. The legacy resolver
|
|
1417
|
+
* ignored this flag for floating tools.
|
|
1418
|
+
* - **QTI gates** — when `qtiEnforcement` is `"on"` (set by host
|
|
1419
|
+
* via {@link setQtiEnforcement}, or auto-promoted when the
|
|
1420
|
+
* bound `AssessmentEntity` / current `AssessmentItemRef`
|
|
1421
|
+
* carries QTI 6-level precedence material — see
|
|
1422
|
+
* {@link resolveEffectiveQtiEnforcement}) **and an assessment
|
|
1423
|
+
* is bound**, the QTI 6-level precedence (district block →
|
|
1424
|
+
* test-admin override → item restriction/requirement →
|
|
1425
|
+
* district requirement → PNP supports / prohibitions) is
|
|
1426
|
+
* applied. `qtiEnforcement: "on"` without a bound assessment
|
|
1427
|
+
* is a no-op for QTI gating.
|
|
1428
|
+
* - **Custom `PolicySource`s** registered via
|
|
1429
|
+
* {@link registerPolicySource}.
|
|
1430
|
+
*
|
|
1431
|
+
* Under the default no-assessment, no-override, no-provider-veto
|
|
1432
|
+
* configuration the result still matches `resolveToolsForLevel(...)`
|
|
1433
|
+
* exactly — a parity asserted by the integration tests in
|
|
1434
|
+
* `tests/policy/coordinator-integration.test.ts`. Once PR 5 deletes
|
|
1435
|
+
* the legacy resolver this method becomes the canonical surface.
|
|
1196
1436
|
*
|
|
1197
|
-
*
|
|
1437
|
+
* Consumers that need the full decision (provenance, diagnostics)
|
|
1438
|
+
* should call {@link decideToolPolicy} instead.
|
|
1198
1439
|
*/
|
|
1199
1440
|
getFloatingTools() {
|
|
1200
1441
|
if (!this.config.tools)
|
|
1201
1442
|
return [];
|
|
1202
|
-
return
|
|
1443
|
+
return this.policyEngine.getVisibleToolIds("section", "*");
|
|
1203
1444
|
}
|
|
1204
1445
|
/**
|
|
1205
1446
|
* Set a callback to be notified when floating tools change.
|
|
@@ -1216,6 +1457,145 @@ export class ToolkitCoordinator {
|
|
|
1216
1457
|
this.floatingToolsChangeCallback = null;
|
|
1217
1458
|
};
|
|
1218
1459
|
}
|
|
1460
|
+
// ----------------------------------------------------------------
|
|
1461
|
+
// M8 PR 2 — Tool Policy Engine surface
|
|
1462
|
+
//
|
|
1463
|
+
// The methods below are additive in PR 2: hosts can already drive
|
|
1464
|
+
// the unified ToolPolicyEngine, but the existing toolbar / section-
|
|
1465
|
+
// player render paths still consume the legacy `resolveToolsForLevel`
|
|
1466
|
+
// / `PnpToolResolver` chain. PR 3 switches consumers; PR 5 deletes
|
|
1467
|
+
// the legacy path. See `.cursor/plans/m8-implementation-plan.md`.
|
|
1468
|
+
// ----------------------------------------------------------------
|
|
1469
|
+
/**
|
|
1470
|
+
* Resolve the visible tool set for a given placement level + scope.
|
|
1471
|
+
*
|
|
1472
|
+
* Thin shim over the owned tool-policy engine. Hosts that need
|
|
1473
|
+
* richer outputs (provenance, diagnostics) read this directly;
|
|
1474
|
+
* hosts that only need the section-level tool IDs can call
|
|
1475
|
+
* {@link getFloatingTools} instead.
|
|
1476
|
+
*/
|
|
1477
|
+
decideToolPolicy(request) {
|
|
1478
|
+
return this.policyEngine.decide(request);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Subscribe to policy-engine change events. Fires whenever the
|
|
1482
|
+
* coordinator's bound inputs change (`updateToolConfig`,
|
|
1483
|
+
* `updateFloatingTools`, `updateAssessment`, `updateCurrentItemRef`,
|
|
1484
|
+
* `setQtiEnforcement`) or a custom `PolicySource` is registered /
|
|
1485
|
+
* removed via {@link registerPolicySource}.
|
|
1486
|
+
*
|
|
1487
|
+
* The listener receives a `ToolPolicyChangeEvent` with the event
|
|
1488
|
+
* `reason` and a frozen snapshot of the engine inputs. Listeners
|
|
1489
|
+
* that want the new visible tool set should call
|
|
1490
|
+
* {@link decideToolPolicy} with their level / scope.
|
|
1491
|
+
*
|
|
1492
|
+
* Note: the engine itself can also emit `reason: "disposed"`, but
|
|
1493
|
+
* the coordinator does not dispose its engine on teardown today,
|
|
1494
|
+
* so subscribers attached via this method will not observe that
|
|
1495
|
+
* reason. Detach via the returned unsubscribe function instead of
|
|
1496
|
+
* relying on a `disposed` event.
|
|
1497
|
+
*/
|
|
1498
|
+
onPolicyChange(listener) {
|
|
1499
|
+
return this.policyEngine.onPolicyChange(listener);
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Bind (or clear) the active QTI assessment for policy decisions.
|
|
1503
|
+
*
|
|
1504
|
+
* Under auto-mode (no host override via {@link setQtiEnforcement}),
|
|
1505
|
+
* the coordinator promotes the engine to `qtiEnforcement: "on"`
|
|
1506
|
+
* iff the assessment carries any QTI 6-level precedence material
|
|
1507
|
+
* (`personalNeedsProfile`, `settings.districtPolicy`,
|
|
1508
|
+
* `settings.testAdministration`) or the currently-bound item ref
|
|
1509
|
+
* carries item-level QTI inputs. A bare assessment record (just
|
|
1510
|
+
* `id` / `name`, no PNP, no settings) keeps `"off"`.
|
|
1511
|
+
*
|
|
1512
|
+
* The host override set via {@link setQtiEnforcement} is sticky
|
|
1513
|
+
* across assessment swaps; calling with `null` clears the binding
|
|
1514
|
+
* and re-runs the auto-mode helper.
|
|
1515
|
+
*/
|
|
1516
|
+
updateAssessment(assessment) {
|
|
1517
|
+
this.boundAssessment = assessment;
|
|
1518
|
+
this.policyEngine.updateInputs({
|
|
1519
|
+
assessment,
|
|
1520
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Bind (or clear) the current item reference for policy decisions.
|
|
1525
|
+
*
|
|
1526
|
+
* Used by item-level QTI gates (item `requiredTools` /
|
|
1527
|
+
* `restrictedTools` / `toolParameters`). Item-level QTI material
|
|
1528
|
+
* also feeds {@link resolveEffectiveQtiEnforcement} — navigating
|
|
1529
|
+
* to an item with QTI settings can flip auto-mode to `"on"` even
|
|
1530
|
+
* when the parent assessment carries no QTI block of its own.
|
|
1531
|
+
*/
|
|
1532
|
+
updateCurrentItemRef(itemRef) {
|
|
1533
|
+
this.boundCurrentItemRef = itemRef;
|
|
1534
|
+
this.policyEngine.updateInputs({
|
|
1535
|
+
currentItemRef: itemRef,
|
|
1536
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Override the auto-mode QTI enforcement decision.
|
|
1541
|
+
*
|
|
1542
|
+
* Pass `"on"` to force QTI enforcement even when no QTI inputs
|
|
1543
|
+
* are bound (useful for tests / fixtures). Pass `"off"` to opt
|
|
1544
|
+
* out even when QTI inputs are present. Pass `null` to clear the
|
|
1545
|
+
* override and return to auto-mode — `"on"` iff
|
|
1546
|
+
* {@link resolveDefaultQtiEnforcement} reports any QTI material
|
|
1547
|
+
* on the bound assessment or current item, otherwise `"off"`.
|
|
1548
|
+
*/
|
|
1549
|
+
setQtiEnforcement(mode) {
|
|
1550
|
+
this.qtiEnforcementOverride = mode;
|
|
1551
|
+
this.policyEngine.updateInputs({
|
|
1552
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Get the policy engine inputs currently driving decisions. Useful
|
|
1557
|
+
* for debugging / instrumentation; do not mutate.
|
|
1558
|
+
*/
|
|
1559
|
+
getPolicyInputs() {
|
|
1560
|
+
return this.policyEngine.getInputs();
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Register a custom {@link PolicySource} with the owned policy
|
|
1564
|
+
* engine. The source participates in every subsequent
|
|
1565
|
+
* {@link decideToolPolicy} call until disposed (the returned
|
|
1566
|
+
* function detaches and emits a `policy-source-removed` event).
|
|
1567
|
+
*
|
|
1568
|
+
* Delegates verbatim to
|
|
1569
|
+
* {@link ToolPolicyEngine.registerPolicySource} — see that
|
|
1570
|
+
* method for the full contract (event ordering, idempotency of
|
|
1571
|
+
* the returned dispose function, and how registered sources
|
|
1572
|
+
* compose with the built-in QTI source).
|
|
1573
|
+
*/
|
|
1574
|
+
registerPolicySource(source) {
|
|
1575
|
+
return this.policyEngine.registerPolicySource(source);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Compute the effective `qtiEnforcement` mode given the explicit
|
|
1579
|
+
* host override and the auto-mode helper.
|
|
1580
|
+
*
|
|
1581
|
+
* Auto-mode (no override) defers to
|
|
1582
|
+
* {@link resolveDefaultQtiEnforcement}, which returns `"on"`
|
|
1583
|
+
* exactly when the bound assessment or current item ref carries
|
|
1584
|
+
* QTI 6-level precedence material (PNP, district policy, test
|
|
1585
|
+
* administration, item-level required/restricted/parameters), and
|
|
1586
|
+
* `"off"` otherwise. Hosts that bind a bare assessment record
|
|
1587
|
+
* (just `id` / `name`) therefore keep the legacy floating-tools
|
|
1588
|
+
* behavior — QTI gates engage the moment QTI material is present.
|
|
1589
|
+
*/
|
|
1590
|
+
resolveEffectiveQtiEnforcement() {
|
|
1591
|
+
if (this.qtiEnforcementOverride !== null) {
|
|
1592
|
+
return this.qtiEnforcementOverride;
|
|
1593
|
+
}
|
|
1594
|
+
return resolveDefaultQtiEnforcement({
|
|
1595
|
+
assessment: this.boundAssessment,
|
|
1596
|
+
currentItemRef: this.boundCurrentItemRef,
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1219
1599
|
/**
|
|
1220
1600
|
* Apply tool configuration changes to underlying services.
|
|
1221
1601
|
* Called after updateToolConfig().
|