@pie-players/pie-assessment-toolkit 0.3.29 → 0.3.31
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 +313 -5
- package/dist/components/ItemToolBar.custom-element.js +362 -176
- package/dist/components/PieAssessmentToolkit.custom-element.js +604 -107
- package/dist/components/SectionToolBar.custom-element.js +0 -36
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/SSMLExtractor.d.ts.map +1 -1
- package/dist/services/SSMLExtractor.js +189 -2
- package/dist/services/SSMLExtractor.js.map +1 -1
- package/dist/services/TTSService.d.ts +7 -0
- package/dist/services/TTSService.d.ts.map +1 -1
- package/dist/services/TTSService.js +15 -0
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ToolRegistry.d.ts.map +1 -1
- package/dist/services/ToolRegistry.js +30 -0
- package/dist/services/ToolRegistry.js.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +264 -6
- package/dist/services/ToolkitCoordinator.d.ts.map +1 -1
- package/dist/services/ToolkitCoordinator.js +350 -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 +85 -0
- package/dist/services/interfaces.d.ts.map +1 -1
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/tool-config-validation.js +1 -1
- package/dist/services/tool-config-validation.js.map +1 -1
- package/dist/services/tools-config-normalizer.d.ts +24 -1
- 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/services/tts-control-events.d.ts +14 -0
- package/dist/services/tts-control-events.d.ts.map +1 -0
- package/dist/services/tts-control-events.js +8 -0
- package/dist/services/tts-control-events.js.map +1 -0
- package/dist/tools/calculators/desmos-provider.js +18 -0
- package/dist/tools/calculators/desmos-provider.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/default-tool-module-loaders.d.ts.map +1 -1
- package/dist/tools/default-tool-module-loaders.js +2 -0
- package/dist/tools/default-tool-module-loaders.js.map +1 -1
- package/dist/tools/types.d.ts +0 -26
- package/dist/tools/types.d.ts.map +1 -1
- package/package.json +25 -8
- 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
|
}
|
|
@@ -751,7 +887,7 @@ export class ToolkitCoordinator {
|
|
|
751
887
|
controller: args.controller,
|
|
752
888
|
});
|
|
753
889
|
await this.hooks.onSectionControllerReady?.(args.context, args.controller);
|
|
754
|
-
await this.emitTelemetry("section-controller-ready", {
|
|
890
|
+
await this.emitTelemetry("pie-toolkit-section-controller-ready", {
|
|
755
891
|
assessmentId: args.key.assessmentId,
|
|
756
892
|
sectionId: args.key.sectionId,
|
|
757
893
|
attemptId: args.key.attemptId,
|
|
@@ -821,7 +957,7 @@ export class ToolkitCoordinator {
|
|
|
821
957
|
}
|
|
822
958
|
await args.controller.dispose?.();
|
|
823
959
|
await this.hooks.onSectionControllerDispose?.(args.context, args.controller);
|
|
824
|
-
await this.emitTelemetry("section-controller-disposed", {
|
|
960
|
+
await this.emitTelemetry("pie-toolkit-section-controller-disposed", {
|
|
825
961
|
assessmentId: args.key.assessmentId,
|
|
826
962
|
sectionId: args.key.sectionId,
|
|
827
963
|
attemptId: args.key.attemptId,
|
|
@@ -873,7 +1009,7 @@ export class ToolkitCoordinator {
|
|
|
873
1009
|
backend: resolvedBackend,
|
|
874
1010
|
},
|
|
875
1011
|
});
|
|
876
|
-
await this.emitTelemetry("tts-init-start", {
|
|
1012
|
+
await this.emitTelemetry("pie-toolkit-tts-init-start", {
|
|
877
1013
|
backend: resolvedBackend,
|
|
878
1014
|
});
|
|
879
1015
|
await this.emitTelemetry("pie-tool-init-start", {
|
|
@@ -887,7 +1023,7 @@ export class ToolkitCoordinator {
|
|
|
887
1023
|
const ttsProvider = await this.ensureProviderReady("tts");
|
|
888
1024
|
const providerInstance = await ttsProvider.createInstance();
|
|
889
1025
|
await this.initializeTTSService(providerInstance, runtimeTTSConfig);
|
|
890
|
-
await this.emitTelemetry("tts-init-success", {
|
|
1026
|
+
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
891
1027
|
provider: "registry",
|
|
892
1028
|
});
|
|
893
1029
|
await this.emitTelemetry("pie-tool-init-success", {
|
|
@@ -926,7 +1062,7 @@ export class ToolkitCoordinator {
|
|
|
926
1062
|
const provider = new BrowserTTSProvider();
|
|
927
1063
|
try {
|
|
928
1064
|
await this.initializeTTSService(provider, runtimeTTSConfig);
|
|
929
|
-
await this.emitTelemetry("tts-init-success", {
|
|
1065
|
+
await this.emitTelemetry("pie-toolkit-tts-init-success", {
|
|
930
1066
|
provider: "browser-fallback",
|
|
931
1067
|
});
|
|
932
1068
|
await this.emitTelemetry("pie-tool-init-success", {
|
|
@@ -938,9 +1074,8 @@ export class ToolkitCoordinator {
|
|
|
938
1074
|
}
|
|
939
1075
|
catch (error) {
|
|
940
1076
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
941
|
-
this.hooks.onTTSError?.(normalized, { phase: "tts-init" });
|
|
942
1077
|
this.handleError(normalized, { phase: "tts-init" });
|
|
943
|
-
await this.emitTelemetry("tts-init-error", {
|
|
1078
|
+
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
944
1079
|
message: normalized.message,
|
|
945
1080
|
});
|
|
946
1081
|
await this.emitTelemetry("pie-tool-init-error", {
|
|
@@ -1013,7 +1148,7 @@ export class ToolkitCoordinator {
|
|
|
1013
1148
|
}
|
|
1014
1149
|
});
|
|
1015
1150
|
const voicesAfterWait = synth.getVoices();
|
|
1016
|
-
await this.emitTelemetry("tts-browser-voices-ready", {
|
|
1151
|
+
await this.emitTelemetry("pie-toolkit-tts-browser-voices-ready", {
|
|
1017
1152
|
voiceCount: voicesAfterWait.length,
|
|
1018
1153
|
timedOut: voicesAfterWait.length === 0,
|
|
1019
1154
|
});
|
|
@@ -1055,7 +1190,7 @@ export class ToolkitCoordinator {
|
|
|
1055
1190
|
if (!this.coordinatorReadyNotified) {
|
|
1056
1191
|
this.coordinatorReadyNotified = true;
|
|
1057
1192
|
await this.hooks.onCoordinatorReady?.(this);
|
|
1058
|
-
await this.emitTelemetry("coordinator-ready", {
|
|
1193
|
+
await this.emitTelemetry("pie-toolkit-coordinator-ready", {
|
|
1059
1194
|
assessmentId: this.assessmentId,
|
|
1060
1195
|
});
|
|
1061
1196
|
}
|
|
@@ -1155,7 +1290,14 @@ export class ToolkitCoordinator {
|
|
|
1155
1290
|
toolRegistry: this.toolRegistry,
|
|
1156
1291
|
});
|
|
1157
1292
|
this.config.tools = validated.config;
|
|
1158
|
-
|
|
1293
|
+
// M8 PR 2 — keep the policy engine's tools input in lockstep
|
|
1294
|
+
// with the validated coordinator config. The engine emits an
|
|
1295
|
+
// `inputs` change event so subscribers (e.g. PR 3 toolbars) can
|
|
1296
|
+
// re-decide without us managing a parallel pub/sub.
|
|
1297
|
+
this.policyEngine.updateInputs({
|
|
1298
|
+
tools: this.config.tools,
|
|
1299
|
+
});
|
|
1300
|
+
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
1159
1301
|
// Apply configuration changes to services
|
|
1160
1302
|
this._applyToolConfigChange(toolId, updates);
|
|
1161
1303
|
}
|
|
@@ -1186,20 +1328,57 @@ export class ToolkitCoordinator {
|
|
|
1186
1328
|
toolRegistry: this.toolRegistry,
|
|
1187
1329
|
});
|
|
1188
1330
|
this.config.tools = validated.config;
|
|
1331
|
+
// M8 PR 2 — keep the policy engine in lockstep with the
|
|
1332
|
+
// floating-tools placement update. See `updateToolConfig` for
|
|
1333
|
+
// the same pattern.
|
|
1334
|
+
this.policyEngine.updateInputs({
|
|
1335
|
+
tools: this.config.tools,
|
|
1336
|
+
});
|
|
1189
1337
|
// Notify listener of change
|
|
1190
1338
|
if (this.floatingToolsChangeCallback) {
|
|
1191
1339
|
this.floatingToolsChangeCallback(toolIds);
|
|
1192
1340
|
}
|
|
1193
1341
|
}
|
|
1194
1342
|
/**
|
|
1195
|
-
* Get currently enabled floating tools.
|
|
1343
|
+
* Get currently enabled floating tools (section-level placement).
|
|
1344
|
+
*
|
|
1345
|
+
* @returns Array of enabled tool IDs.
|
|
1346
|
+
*
|
|
1347
|
+
* @remarks
|
|
1348
|
+
* As of M8 PR 2 this routes through the {@link ToolPolicyEngine}
|
|
1349
|
+
* rather than the legacy `resolveToolsForLevel(...)` shim. The two
|
|
1350
|
+
* paths agree on `placement → policy.allowed → policy.blocked`,
|
|
1351
|
+
* but the engine additionally enforces:
|
|
1352
|
+
*
|
|
1353
|
+
* - **Provider veto** — `tools.providers[id].enabled === false`
|
|
1354
|
+
* removes the tool from the visible set. The legacy resolver
|
|
1355
|
+
* ignored this flag for floating tools.
|
|
1356
|
+
* - **QTI gates** — when `qtiEnforcement` is `"on"` (set by host
|
|
1357
|
+
* via {@link setQtiEnforcement}, or auto-promoted when the
|
|
1358
|
+
* bound `AssessmentEntity` / current `AssessmentItemRef`
|
|
1359
|
+
* carries QTI 6-level precedence material — see
|
|
1360
|
+
* {@link resolveEffectiveQtiEnforcement}) **and an assessment
|
|
1361
|
+
* is bound**, the QTI 6-level precedence (district block →
|
|
1362
|
+
* test-admin override → item restriction/requirement →
|
|
1363
|
+
* district requirement → PNP supports / prohibitions) is
|
|
1364
|
+
* applied. `qtiEnforcement: "on"` without a bound assessment
|
|
1365
|
+
* is a no-op for QTI gating.
|
|
1366
|
+
* - **Custom `PolicySource`s** registered via
|
|
1367
|
+
* {@link registerPolicySource}.
|
|
1368
|
+
*
|
|
1369
|
+
* Under the default no-assessment, no-override, no-provider-veto
|
|
1370
|
+
* configuration the result still matches `resolveToolsForLevel(...)`
|
|
1371
|
+
* exactly — a parity asserted by the integration tests in
|
|
1372
|
+
* `tests/policy/coordinator-integration.test.ts`. Once PR 5 deletes
|
|
1373
|
+
* the legacy resolver this method becomes the canonical surface.
|
|
1196
1374
|
*
|
|
1197
|
-
*
|
|
1375
|
+
* Consumers that need the full decision (provenance, diagnostics)
|
|
1376
|
+
* should call {@link decideToolPolicy} instead.
|
|
1198
1377
|
*/
|
|
1199
1378
|
getFloatingTools() {
|
|
1200
1379
|
if (!this.config.tools)
|
|
1201
1380
|
return [];
|
|
1202
|
-
return
|
|
1381
|
+
return this.policyEngine.getVisibleToolIds("section", "*");
|
|
1203
1382
|
}
|
|
1204
1383
|
/**
|
|
1205
1384
|
* Set a callback to be notified when floating tools change.
|
|
@@ -1216,6 +1395,145 @@ export class ToolkitCoordinator {
|
|
|
1216
1395
|
this.floatingToolsChangeCallback = null;
|
|
1217
1396
|
};
|
|
1218
1397
|
}
|
|
1398
|
+
// ----------------------------------------------------------------
|
|
1399
|
+
// M8 PR 2 — Tool Policy Engine surface
|
|
1400
|
+
//
|
|
1401
|
+
// The methods below are additive in PR 2: hosts can already drive
|
|
1402
|
+
// the unified ToolPolicyEngine, but the existing toolbar / section-
|
|
1403
|
+
// player render paths still consume the legacy `resolveToolsForLevel`
|
|
1404
|
+
// / `PnpToolResolver` chain. PR 3 switches consumers; PR 5 deletes
|
|
1405
|
+
// the legacy path. See `.cursor/plans/m8-implementation-plan.md`.
|
|
1406
|
+
// ----------------------------------------------------------------
|
|
1407
|
+
/**
|
|
1408
|
+
* Resolve the visible tool set for a given placement level + scope.
|
|
1409
|
+
*
|
|
1410
|
+
* Thin shim over the owned tool-policy engine. Hosts that need
|
|
1411
|
+
* richer outputs (provenance, diagnostics) read this directly;
|
|
1412
|
+
* hosts that only need the section-level tool IDs can call
|
|
1413
|
+
* {@link getFloatingTools} instead.
|
|
1414
|
+
*/
|
|
1415
|
+
decideToolPolicy(request) {
|
|
1416
|
+
return this.policyEngine.decide(request);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Subscribe to policy-engine change events. Fires whenever the
|
|
1420
|
+
* coordinator's bound inputs change (`updateToolConfig`,
|
|
1421
|
+
* `updateFloatingTools`, `updateAssessment`, `updateCurrentItemRef`,
|
|
1422
|
+
* `setQtiEnforcement`) or a custom `PolicySource` is registered /
|
|
1423
|
+
* removed via {@link registerPolicySource}.
|
|
1424
|
+
*
|
|
1425
|
+
* The listener receives a `ToolPolicyChangeEvent` with the event
|
|
1426
|
+
* `reason` and a frozen snapshot of the engine inputs. Listeners
|
|
1427
|
+
* that want the new visible tool set should call
|
|
1428
|
+
* {@link decideToolPolicy} with their level / scope.
|
|
1429
|
+
*
|
|
1430
|
+
* Note: the engine itself can also emit `reason: "disposed"`, but
|
|
1431
|
+
* the coordinator does not dispose its engine on teardown today,
|
|
1432
|
+
* so subscribers attached via this method will not observe that
|
|
1433
|
+
* reason. Detach via the returned unsubscribe function instead of
|
|
1434
|
+
* relying on a `disposed` event.
|
|
1435
|
+
*/
|
|
1436
|
+
onPolicyChange(listener) {
|
|
1437
|
+
return this.policyEngine.onPolicyChange(listener);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Bind (or clear) the active QTI assessment for policy decisions.
|
|
1441
|
+
*
|
|
1442
|
+
* Under auto-mode (no host override via {@link setQtiEnforcement}),
|
|
1443
|
+
* the coordinator promotes the engine to `qtiEnforcement: "on"`
|
|
1444
|
+
* iff the assessment carries any QTI 6-level precedence material
|
|
1445
|
+
* (`personalNeedsProfile`, `settings.districtPolicy`,
|
|
1446
|
+
* `settings.testAdministration`) or the currently-bound item ref
|
|
1447
|
+
* carries item-level QTI inputs. A bare assessment record (just
|
|
1448
|
+
* `id` / `name`, no PNP, no settings) keeps `"off"`.
|
|
1449
|
+
*
|
|
1450
|
+
* The host override set via {@link setQtiEnforcement} is sticky
|
|
1451
|
+
* across assessment swaps; calling with `null` clears the binding
|
|
1452
|
+
* and re-runs the auto-mode helper.
|
|
1453
|
+
*/
|
|
1454
|
+
updateAssessment(assessment) {
|
|
1455
|
+
this.boundAssessment = assessment;
|
|
1456
|
+
this.policyEngine.updateInputs({
|
|
1457
|
+
assessment,
|
|
1458
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Bind (or clear) the current item reference for policy decisions.
|
|
1463
|
+
*
|
|
1464
|
+
* Used by item-level QTI gates (item `requiredTools` /
|
|
1465
|
+
* `restrictedTools` / `toolParameters`). Item-level QTI material
|
|
1466
|
+
* also feeds {@link resolveEffectiveQtiEnforcement} — navigating
|
|
1467
|
+
* to an item with QTI settings can flip auto-mode to `"on"` even
|
|
1468
|
+
* when the parent assessment carries no QTI block of its own.
|
|
1469
|
+
*/
|
|
1470
|
+
updateCurrentItemRef(itemRef) {
|
|
1471
|
+
this.boundCurrentItemRef = itemRef;
|
|
1472
|
+
this.policyEngine.updateInputs({
|
|
1473
|
+
currentItemRef: itemRef,
|
|
1474
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Override the auto-mode QTI enforcement decision.
|
|
1479
|
+
*
|
|
1480
|
+
* Pass `"on"` to force QTI enforcement even when no QTI inputs
|
|
1481
|
+
* are bound (useful for tests / fixtures). Pass `"off"` to opt
|
|
1482
|
+
* out even when QTI inputs are present. Pass `null` to clear the
|
|
1483
|
+
* override and return to auto-mode — `"on"` iff
|
|
1484
|
+
* {@link resolveDefaultQtiEnforcement} reports any QTI material
|
|
1485
|
+
* on the bound assessment or current item, otherwise `"off"`.
|
|
1486
|
+
*/
|
|
1487
|
+
setQtiEnforcement(mode) {
|
|
1488
|
+
this.qtiEnforcementOverride = mode;
|
|
1489
|
+
this.policyEngine.updateInputs({
|
|
1490
|
+
qtiEnforcement: this.resolveEffectiveQtiEnforcement(),
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Get the policy engine inputs currently driving decisions. Useful
|
|
1495
|
+
* for debugging / instrumentation; do not mutate.
|
|
1496
|
+
*/
|
|
1497
|
+
getPolicyInputs() {
|
|
1498
|
+
return this.policyEngine.getInputs();
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Register a custom {@link PolicySource} with the owned policy
|
|
1502
|
+
* engine. The source participates in every subsequent
|
|
1503
|
+
* {@link decideToolPolicy} call until disposed (the returned
|
|
1504
|
+
* function detaches and emits a `policy-source-removed` event).
|
|
1505
|
+
*
|
|
1506
|
+
* Delegates verbatim to
|
|
1507
|
+
* {@link ToolPolicyEngine.registerPolicySource} — see that
|
|
1508
|
+
* method for the full contract (event ordering, idempotency of
|
|
1509
|
+
* the returned dispose function, and how registered sources
|
|
1510
|
+
* compose with the built-in QTI source).
|
|
1511
|
+
*/
|
|
1512
|
+
registerPolicySource(source) {
|
|
1513
|
+
return this.policyEngine.registerPolicySource(source);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Compute the effective `qtiEnforcement` mode given the explicit
|
|
1517
|
+
* host override and the auto-mode helper.
|
|
1518
|
+
*
|
|
1519
|
+
* Auto-mode (no override) defers to
|
|
1520
|
+
* {@link resolveDefaultQtiEnforcement}, which returns `"on"`
|
|
1521
|
+
* exactly when the bound assessment or current item ref carries
|
|
1522
|
+
* QTI 6-level precedence material (PNP, district policy, test
|
|
1523
|
+
* administration, item-level required/restricted/parameters), and
|
|
1524
|
+
* `"off"` otherwise. Hosts that bind a bare assessment record
|
|
1525
|
+
* (just `id` / `name`) therefore keep the legacy floating-tools
|
|
1526
|
+
* behavior — QTI gates engage the moment QTI material is present.
|
|
1527
|
+
*/
|
|
1528
|
+
resolveEffectiveQtiEnforcement() {
|
|
1529
|
+
if (this.qtiEnforcementOverride !== null) {
|
|
1530
|
+
return this.qtiEnforcementOverride;
|
|
1531
|
+
}
|
|
1532
|
+
return resolveDefaultQtiEnforcement({
|
|
1533
|
+
assessment: this.boundAssessment,
|
|
1534
|
+
currentItemRef: this.boundCurrentItemRef,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1219
1537
|
/**
|
|
1220
1538
|
* Apply tool configuration changes to underlying services.
|
|
1221
1539
|
* Called after updateToolConfig().
|