@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +451 -541
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +0 -1
- package/dist/attempt/TestSession.js +10 -5
- package/dist/components/ItemScope.custom-element.js +1 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
- package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
- package/dist/components/chunks/ItemToolBar-asenkvx6.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -10
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
- package/dist/policy/core/ToolPolicyEngine.js +73 -16
- package/dist/policy/core/compose-decision.d.ts +3 -3
- package/dist/policy/core/compose-decision.js +32 -30
- package/dist/policy/core/decision-types.d.ts +51 -34
- package/dist/policy/core/feature-decision.d.ts +17 -15
- package/dist/policy/core/feature-decision.js +11 -13
- package/dist/policy/core/pnp-policy-inputs.d.ts +7 -3
- package/dist/policy/core/pnp-policy-inputs.js +17 -33
- package/dist/policy/core/provenance.d.ts +2 -2
- package/dist/policy/core/tool-parameters.d.ts +12 -0
- package/dist/policy/core/tool-parameters.js +27 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
- package/dist/policy/sources/PnpPolicySource.js +57 -29
- package/dist/runtime/SectionControllerBinding.d.ts +6 -2
- package/dist/runtime/SectionControllerBinding.js +21 -15
- package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
- package/dist/runtime/adapter/subscriber-fanout.js +1 -4
- package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
- package/dist/runtime/composition-emit-scheduler.js +1 -1
- package/dist/runtime/content-region.d.ts +0 -4
- package/dist/runtime/content-region.js +2 -2
- package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
- package/dist/runtime/core/SectionEngineCore.js +0 -4
- package/dist/runtime/core/cohort.d.ts +0 -5
- package/dist/runtime/core/cohort.js +0 -9
- package/dist/runtime/core/engine-output.d.ts +3 -2
- package/dist/runtime/core/engine-transition.js +3 -2
- package/dist/runtime/registration-events.d.ts +11 -14
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-scope.d.ts +4 -4
- package/dist/runtime/shell-scope.js +15 -9
- package/dist/runtime/tool-host-contract.d.ts +0 -4
- package/dist/runtime/tool-host-contract.js +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +32 -61
- package/dist/services/ElementToolStateStore.js +37 -67
- package/dist/services/HighlightCoordinator.d.ts +9 -21
- package/dist/services/HighlightCoordinator.js +52 -80
- package/dist/services/RangeSerializer.d.ts +4 -35
- package/dist/services/RangeSerializer.js +4 -48
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +51 -57
- package/dist/services/TTSService.js +323 -284
- package/dist/services/ToolCoordinator.d.ts +6 -49
- package/dist/services/ToolCoordinator.js +25 -61
- package/dist/services/ToolRegistry.d.ts +83 -80
- package/dist/services/ToolRegistry.js +91 -67
- package/dist/services/ToolkitCoordinator.d.ts +83 -81
- package/dist/services/ToolkitCoordinator.js +186 -174
- package/dist/services/framework-error.d.ts +30 -7
- package/dist/services/framework-error.js +10 -13
- package/dist/services/interfaces.d.ts +51 -71
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.js +6 -8
- package/dist/services/spoken-audio-cards.d.ts +2 -4
- package/dist/services/spoken-audio-cards.js +2 -4
- package/dist/services/tool-config-validation.d.ts +5 -7
- package/dist/services/tool-config-validation.js +11 -26
- package/dist/services/tool-context.d.ts +39 -24
- package/dist/services/tool-context.js +158 -92
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +1 -2
- package/dist/services/tool-icons.js +2 -2
- package/dist/services/tool-instance-id.d.ts +8 -6
- package/dist/services/tool-instance-id.js +12 -18
- package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
- package/dist/services/tool-providers/TTSToolProvider.js +7 -63
- package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
- package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
- package/dist/services/tool-request.d.ts +4 -0
- package/dist/services/tool-request.js +7 -2
- package/dist/services/toolbar-decided-tools.d.ts +2 -0
- package/dist/services/toolbar-decided-tools.js +2 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +18 -9
- package/dist/services/tools-config-normalizer.js +25 -19
- package/dist/services/tts/browser-provider.d.ts +2 -5
- package/dist/services/tts/browser-provider.js +13 -22
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +0 -7
- package/dist/services/tts/flat-tree.js +2 -2
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.js +10 -29
- package/dist/services/tts/math-speech.d.ts +1 -1
- package/dist/services/tts/math-speech.js +3 -0
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +12 -1
- package/dist/services/tts/ssml/spoken-text.js +1 -1
- package/dist/services/tts/start-failure.d.ts +0 -7
- package/dist/services/tts/start-failure.js +1 -1
- package/dist/services/tts/text-segmentation.d.ts +5 -0
- package/dist/services/tts/text-segmentation.js +13 -10
- package/dist/services/tts-runtime-config.d.ts +32 -12
- package/dist/services/tts-runtime-config.js +14 -20
- package/dist/tools/content-capability-resolution.d.ts +2 -2
- package/dist/tools/registration.d.ts +7 -6
- package/dist/tools/registration.js +10 -9
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
- package/dist/tools/tool-surface-host.js +1 -8
- package/package.json +8 -6
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
- package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ToolkitCoordinator
|
|
2
|
+
* ToolkitCoordinator: one per assessment, the owner of every toolkit service
|
|
3
|
+
* (TTSService, ToolCoordinator, HighlightCoordinator, ElementToolStateStore,
|
|
4
|
+
* AccessibilityCatalogResolver, ToolProviderRegistry), the ToolPolicyEngine,
|
|
5
|
+
* the framework-error bus and the section controllers. It holds toolkit
|
|
6
|
+
* concerns only; navigation, timing and progress belong to the player.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Layout: the module's exported config, hook and event types come first, then
|
|
9
|
+
* the class, whose members run in this order:
|
|
10
|
+
* - config resolution and the constructor;
|
|
11
|
+
* - error, telemetry and tool-failure reporting, readiness listeners and the
|
|
12
|
+
* framework-error bus;
|
|
13
|
+
* - element tool-state persistence;
|
|
14
|
+
* - tool registry adoption and provider registration;
|
|
15
|
+
* - section-event subscriptions, which follow the active cohort;
|
|
16
|
+
* - section controller creation, publication and disposal;
|
|
17
|
+
* - coordinator disposal;
|
|
18
|
+
* - TTS bring-up and readiness;
|
|
19
|
+
* - tool config updates and the policy engine surface, with the tool-context
|
|
20
|
+
* resolvers split around it (installation before, queries after);
|
|
21
|
+
* - tool-open requests;
|
|
22
|
+
* - config-change application and TTS reconfiguration, last.
|
|
15
23
|
*/
|
|
16
24
|
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
17
25
|
import { collectToolConfigDiagnostics, normalizeToolConfigStrictness, reportToolConfigDiagnostics, } from "./tool-config-validation.js";
|
|
@@ -51,6 +59,16 @@ const mergeToolConfigUpdate = (toolId, current, updates) => {
|
|
|
51
59
|
}
|
|
52
60
|
return next;
|
|
53
61
|
};
|
|
62
|
+
const TOOL_FAILURE_SUMMARY = {
|
|
63
|
+
"tool-module-load": "failed to load",
|
|
64
|
+
"tool-request-open": "failed to open on request",
|
|
65
|
+
"tool-request-host-check": "failed its toolbar's host check",
|
|
66
|
+
"tool-visibility": "failed its relevance check",
|
|
67
|
+
"tool-applicability": "failed its applicability check",
|
|
68
|
+
"tool-state-load": "failed to restore its state",
|
|
69
|
+
"tool-state-save": "failed to save its state",
|
|
70
|
+
"tool-playback": "failed during playback",
|
|
71
|
+
};
|
|
54
72
|
const SECTION_ITEM_EVENT_TYPES = [
|
|
55
73
|
"item-selected",
|
|
56
74
|
"item-session-data-changed",
|
|
@@ -74,10 +92,8 @@ const SECTION_SCOPED_EVENT_TYPES = [
|
|
|
74
92
|
*
|
|
75
93
|
* @example
|
|
76
94
|
* ```typescript
|
|
77
|
-
* //
|
|
78
|
-
* const toolRegistry = createPackagedToolRegistry(
|
|
79
|
-
* toolModuleLoaders: DEFAULT_TOOL_MODULE_LOADERS,
|
|
80
|
-
* });
|
|
95
|
+
* // Exported by @pie-players/pie-default-tool-loaders
|
|
96
|
+
* const toolRegistry = createPackagedToolRegistry();
|
|
81
97
|
*
|
|
82
98
|
* // Create coordinator with configuration
|
|
83
99
|
* const coordinator = new ToolkitCoordinator({
|
|
@@ -122,7 +138,6 @@ export class ToolkitCoordinator {
|
|
|
122
138
|
ttsDegraded = false;
|
|
123
139
|
ttsInitPromise;
|
|
124
140
|
ttsReconfigurePromise;
|
|
125
|
-
stateLoaded = false;
|
|
126
141
|
/**
|
|
127
142
|
* The loader ran, whether or not it returned state. A failed load is
|
|
128
143
|
* reported once and the coordinator runs without saved tool state.
|
|
@@ -137,8 +152,8 @@ export class ToolkitCoordinator {
|
|
|
137
152
|
* tools reports its failure again as fatal.
|
|
138
153
|
*/
|
|
139
154
|
degradedTools = new Map();
|
|
140
|
-
/**
|
|
141
|
-
|
|
155
|
+
/** Tool failures already reported, by phase and tool, each reported once. */
|
|
156
|
+
reportedToolFailures = new Set();
|
|
142
157
|
readyChangeListeners = new Set();
|
|
143
158
|
eagerInit;
|
|
144
159
|
toolRegistry;
|
|
@@ -157,7 +172,7 @@ export class ToolkitCoordinator {
|
|
|
157
172
|
toolRegistryAbsent = false;
|
|
158
173
|
toolContextResolvers = new Map();
|
|
159
174
|
toolContextResolverChangeListeners = new Set();
|
|
160
|
-
toolRequests = new ToolRequestRegistry();
|
|
175
|
+
toolRequests = new ToolRequestRegistry((toolId, phase, error) => this.reportToolFailure(toolId, phase, error));
|
|
161
176
|
sectionControllers = new Map();
|
|
162
177
|
sectionControllerKeys = new Map();
|
|
163
178
|
sectionControllerInitEntries = new Map();
|
|
@@ -165,8 +180,8 @@ export class ToolkitCoordinator {
|
|
|
165
180
|
sectionPersistenceStrategies = new Map();
|
|
166
181
|
sectionControllerLifecycleListeners = new Set();
|
|
167
182
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
183
|
+
* Every active section-event subscription, indexed by listener
|
|
184
|
+
* identity. The listener follows the toolkit's active
|
|
170
185
|
* section cohort across transitions; this map is the registry the
|
|
171
186
|
* coordinator iterates on every cohort change to detach the listener
|
|
172
187
|
* from the outgoing controller, attach it to the new one, and replay
|
|
@@ -178,8 +193,8 @@ export class ToolkitCoordinator {
|
|
|
178
193
|
*/
|
|
179
194
|
activeSubscriptions = new Map();
|
|
180
195
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
196
|
+
* Map key of the section controller currently treated as the
|
|
197
|
+
* *active cohort*. Set by `getOrCreateSectionController` (both
|
|
183
198
|
* the create-new and resolve-existing paths) and cleared when the
|
|
184
199
|
* matching controller is disposed, and `null` while the next requested
|
|
185
200
|
* cohort is still starting.
|
|
@@ -196,6 +211,8 @@ export class ToolkitCoordinator {
|
|
|
196
211
|
telemetryListeners = new Set();
|
|
197
212
|
frameworkErrorBus;
|
|
198
213
|
ownsFrameworkErrorBus;
|
|
214
|
+
/** The instance behind {@link toolCoordinator}, kept for disposal. */
|
|
215
|
+
ownedToolCoordinator;
|
|
199
216
|
frameworkErrorHookUnsubscribe = null;
|
|
200
217
|
disposePromise = null;
|
|
201
218
|
/**
|
|
@@ -206,20 +223,12 @@ export class ToolkitCoordinator {
|
|
|
206
223
|
*/
|
|
207
224
|
policyEngine;
|
|
208
225
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* from the policy material the bound assessment and the decision's item carry.
|
|
212
|
-
* `"on"` / `"off"` are explicit host opt-in / opt-out and stick
|
|
213
|
-
* across subsequent assessment swaps until the host clears
|
|
214
|
-
* the override by calling `setPnpEnforcement(null)`.
|
|
215
|
-
*/
|
|
216
|
-
pnpEnforcementOverride = null;
|
|
217
|
-
/**
|
|
218
|
-
* Policy diagnostics already logged, keyed by code, tool id and, for
|
|
219
|
-
* `tool-policy.itemSettingNotApplied`, item id; see
|
|
226
|
+
* Policy diagnostics reported since the last input change, keyed by code,
|
|
227
|
+
* tool id and, for `tool-policy.itemSettingNotApplied`, item id; see
|
|
220
228
|
* {@link warnPolicyDiagnostics}.
|
|
221
229
|
*/
|
|
222
|
-
|
|
230
|
+
reportedPolicyDiagnostics = new Map();
|
|
231
|
+
policyDiagnosticListeners = new Set();
|
|
223
232
|
/**
|
|
224
233
|
* Whether {@link decideFeaturePolicy} has already reported serving a decision
|
|
225
234
|
* with no assessment bound.
|
|
@@ -324,7 +333,6 @@ export class ToolkitCoordinator {
|
|
|
324
333
|
this.hooks = { ...resolvedConfig.hooks };
|
|
325
334
|
this.lazyInit = config.lazyInit === true;
|
|
326
335
|
this.eagerInit = config.eagerInit ?? !this.lazyInit;
|
|
327
|
-
this.pnpEnforcementOverride = this.resolveConfiguredPnpEnforcement();
|
|
328
336
|
// Use the host-provided framework-error bus if one was passed
|
|
329
337
|
// (typical when embedded inside <pie-assessment-toolkit>, so
|
|
330
338
|
// pre-coordinator failures from the CE flow through the same
|
|
@@ -333,15 +341,13 @@ export class ToolkitCoordinator {
|
|
|
333
341
|
config.frameworkErrorBus ?? new FrameworkErrorBus();
|
|
334
342
|
this.ownsFrameworkErrorBus = !config.frameworkErrorBus;
|
|
335
343
|
this.subscribeFrameworkErrorHookAdapters();
|
|
336
|
-
|
|
337
|
-
this.toolCoordinator =
|
|
344
|
+
this.ownedToolCoordinator = new ToolCoordinator();
|
|
345
|
+
this.toolCoordinator = this.ownedToolCoordinator;
|
|
338
346
|
this.highlightCoordinator = new HighlightCoordinator();
|
|
339
347
|
this.elementToolStateStore = new ElementToolStateStore();
|
|
340
348
|
this.catalogResolver = new AccessibilityCatalogResolver(resolvedConfig.accessibility?.catalogs || [], resolvedConfig.accessibility?.language || "en-US");
|
|
341
|
-
// Initialize tool provider registry
|
|
342
349
|
this.toolProviderRegistry = new ToolProviderRegistry();
|
|
343
350
|
this._registerToolProviders();
|
|
344
|
-
// Initialize TTS service based on config
|
|
345
351
|
this.ttsService = new TTSService();
|
|
346
352
|
// Selection read-aloud speaks through this service without the inline TTS
|
|
347
353
|
// tool ever having run, so it cannot rely on that tool to attach highlights.
|
|
@@ -359,15 +365,23 @@ export class ToolkitCoordinator {
|
|
|
359
365
|
this.policyEngine = new ToolPolicyEngine({
|
|
360
366
|
toolRegistry: this.toolRegistry,
|
|
361
367
|
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
368
|
+
assessmentOptional: this.config.assessmentOptional === true,
|
|
362
369
|
inputs: {
|
|
363
370
|
tools: this.config.tools,
|
|
364
371
|
assessment: null,
|
|
365
|
-
pnpEnforcement: this.
|
|
372
|
+
pnpEnforcement: this.resolveConfiguredPnpEnforcement(),
|
|
366
373
|
},
|
|
367
374
|
});
|
|
375
|
+
// Input diagnostics are computed once per change, so they are reported
|
|
376
|
+
// here rather than with each decision.
|
|
368
377
|
this.policyEngine.onPolicyChange((event) => {
|
|
369
|
-
if (event.reason
|
|
370
|
-
|
|
378
|
+
if (event.reason === "disposed")
|
|
379
|
+
return;
|
|
380
|
+
if (event.reason === "inputs" || event.reason === "pnp-enforcement") {
|
|
381
|
+
this.reportedPolicyDiagnostics.clear();
|
|
382
|
+
}
|
|
383
|
+
this.warnPolicyDiagnostics(event.inputs.diagnostics);
|
|
384
|
+
this.reportNewlyGrantedFailures();
|
|
371
385
|
});
|
|
372
386
|
if (this.eagerInit) {
|
|
373
387
|
void this.waitUntilReady().catch((err) => {
|
|
@@ -460,25 +474,30 @@ export class ToolkitCoordinator {
|
|
|
460
474
|
}
|
|
461
475
|
/**
|
|
462
476
|
* Every report site states whether the coordinator carries on after the
|
|
463
|
-
* failure; one that takes down only its section passes
|
|
477
|
+
* failure; one that takes down only its section passes that section.
|
|
464
478
|
*/
|
|
465
|
-
handleError(error, context,
|
|
479
|
+
handleError(error, context, cohort) {
|
|
466
480
|
const model = frameworkErrorFromCoordinatorContext({
|
|
467
481
|
error,
|
|
468
482
|
context,
|
|
469
483
|
recoverable: context.recoverable,
|
|
470
|
-
scope,
|
|
484
|
+
scope: cohort ? "cohort" : "runtime",
|
|
485
|
+
cohort,
|
|
471
486
|
});
|
|
472
487
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
473
488
|
}
|
|
474
489
|
/**
|
|
475
490
|
* Whether policy grants `toolId` as an accommodation on some surface: a
|
|
476
491
|
* test-administration override, a mounted item's or a district requirement,
|
|
477
|
-
* or profile support.
|
|
492
|
+
* or profile support. A toolbar tool counts only surfaces where PNP
|
|
493
|
+
* enforcement is on; a region feature ignores enforcement, as its decisions
|
|
494
|
+
* do. Read without the unbound-assessment warning.
|
|
478
495
|
*/
|
|
479
496
|
isToolGranted(toolId) {
|
|
480
497
|
try {
|
|
481
|
-
return this.policyEngine.grantsFeatureAnywhere(toolId
|
|
498
|
+
return this.policyEngine.grantsFeatureAnywhere(toolId, {
|
|
499
|
+
enforced: this.toolRegistry.getToolActivation(toolId) !== "region",
|
|
500
|
+
});
|
|
482
501
|
}
|
|
483
502
|
catch {
|
|
484
503
|
return false;
|
|
@@ -490,7 +509,7 @@ export class ToolkitCoordinator {
|
|
|
490
509
|
* `fallbackFollows` marks a failure another path absorbs, which is recoverable
|
|
491
510
|
* whatever the policy.
|
|
492
511
|
*/
|
|
493
|
-
|
|
512
|
+
reportStartFailure(error, context, toolIds, fallbackFollows = false) {
|
|
494
513
|
const ids = Array.from(toolIds);
|
|
495
514
|
const recoverable = fallbackFollows || !ids.some((toolId) => this.isToolGranted(toolId));
|
|
496
515
|
if (recoverable && !fallbackFollows) {
|
|
@@ -500,21 +519,32 @@ export class ToolkitCoordinator {
|
|
|
500
519
|
this.handleError(error, { ...context, recoverable });
|
|
501
520
|
}
|
|
502
521
|
/**
|
|
503
|
-
* Report that a
|
|
504
|
-
* the start-failure policy: the tool degrades, so
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
522
|
+
* Report that a tool failed in `phase`. A module that failed to load follows
|
|
523
|
+
* the start-failure policy: the tool degrades, so toolbars withhold it, unless
|
|
524
|
+
* policy grants it. Every other phase is recoverable, since its report site
|
|
525
|
+
* keeps a recovery, and is reported on a microtask. Reported once per tool and
|
|
526
|
+
* phase however many toolbars or instances fail. A module failure re-announces the request targets, since
|
|
527
|
+
* the reporting toolbar no longer hosts the tool.
|
|
508
528
|
*/
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
529
|
+
reportToolFailure(toolId, phase, error) {
|
|
530
|
+
if (phase === "tool-module-load")
|
|
531
|
+
this.toolRequests.notifyTargetsChange();
|
|
532
|
+
const key = `${phase}\u0000${toolId}`;
|
|
533
|
+
if (this.reportedToolFailures.has(key))
|
|
512
534
|
return;
|
|
513
|
-
this.
|
|
535
|
+
this.reportedToolFailures.add(key);
|
|
514
536
|
const detail = error instanceof Error && error.message.trim().length > 0
|
|
515
537
|
? error.message
|
|
516
538
|
: String(error);
|
|
517
|
-
this.
|
|
539
|
+
const report = () => this.reportStartFailure(new Error(`Tool "${toolId}" ${TOOL_FAILURE_SUMMARY[phase]}: ${detail}`, {
|
|
540
|
+
cause: error,
|
|
541
|
+
}), { phase, details: { toolId } }, [toolId], phase !== "tool-module-load");
|
|
542
|
+
// The recoverable phases fail inside a toolbar's derived state, where a
|
|
543
|
+
// listener writing state would throw.
|
|
544
|
+
if (phase === "tool-module-load")
|
|
545
|
+
report();
|
|
546
|
+
else
|
|
547
|
+
queueMicrotask(report);
|
|
518
548
|
}
|
|
519
549
|
reportNewlyGrantedFailures() {
|
|
520
550
|
for (const [toolId, failure] of this.degradedTools) {
|
|
@@ -573,6 +603,13 @@ export class ToolkitCoordinator {
|
|
|
573
603
|
reportFrameworkError(model) {
|
|
574
604
|
this.frameworkErrorBus.reportFrameworkError(model);
|
|
575
605
|
}
|
|
606
|
+
sectionControllerKey(args) {
|
|
607
|
+
return {
|
|
608
|
+
assessmentId: this.assessmentId,
|
|
609
|
+
sectionId: args.sectionId,
|
|
610
|
+
attemptId: args.attemptId,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
576
613
|
getSectionControllerMapKey(key) {
|
|
577
614
|
return `${key.assessmentId}::${key.sectionId}::${key.attemptId || ""}`;
|
|
578
615
|
}
|
|
@@ -662,7 +699,6 @@ export class ToolkitCoordinator {
|
|
|
662
699
|
if (state && typeof state === "object") {
|
|
663
700
|
this.elementToolStateStore.loadState(state);
|
|
664
701
|
}
|
|
665
|
-
this.stateLoaded = true;
|
|
666
702
|
this.stateLoadSettled = true;
|
|
667
703
|
await this.emitTelemetry("pie-toolkit-tool-state-loaded", {
|
|
668
704
|
hasState: Boolean(state),
|
|
@@ -754,9 +790,6 @@ export class ToolkitCoordinator {
|
|
|
754
790
|
this.scheduleTTSReconfigure();
|
|
755
791
|
return true;
|
|
756
792
|
}
|
|
757
|
-
/**
|
|
758
|
-
* Register tool providers in the registry
|
|
759
|
-
*/
|
|
760
793
|
_registerToolProviders() {
|
|
761
794
|
const descriptorTools = this.getProviderDescriptorTools();
|
|
762
795
|
for (const tool of descriptorTools) {
|
|
@@ -853,7 +886,7 @@ export class ToolkitCoordinator {
|
|
|
853
886
|
*/
|
|
854
887
|
reportProviderRegisterFailure(err, toolId) {
|
|
855
888
|
console.warn(`[ToolkitCoordinator] Failed to register the provider of tool "${toolId}":`, err);
|
|
856
|
-
this.
|
|
889
|
+
this.reportStartFailure(err, { phase: "provider-register", toolId }, [toolId]);
|
|
857
890
|
}
|
|
858
891
|
async ensureProviderReady(toolId) {
|
|
859
892
|
return this.initializeProvider(toolId, false);
|
|
@@ -890,7 +923,7 @@ export class ToolkitCoordinator {
|
|
|
890
923
|
if (err instanceof ToolkitCoordinatorDisposedError)
|
|
891
924
|
throw err;
|
|
892
925
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
893
|
-
this.
|
|
926
|
+
this.reportStartFailure(error, { phase: "provider-init", toolId },
|
|
894
927
|
// Attributed when the id names a tool with a provider; any other id
|
|
895
928
|
// was never a tool's.
|
|
896
929
|
this.toolRegistry.get(toolId)?.provider ? [toolId] : [], fallbackFollows);
|
|
@@ -920,21 +953,15 @@ export class ToolkitCoordinator {
|
|
|
920
953
|
};
|
|
921
954
|
}
|
|
922
955
|
getSectionController(args) {
|
|
923
|
-
const key =
|
|
924
|
-
assessmentId: this.assessmentId,
|
|
925
|
-
sectionId: args.sectionId,
|
|
926
|
-
attemptId: args.attemptId,
|
|
927
|
-
};
|
|
956
|
+
const key = this.sectionControllerKey(args);
|
|
928
957
|
return this.sectionControllers.get(this.getSectionControllerMapKey(key));
|
|
929
958
|
}
|
|
930
959
|
subscribeSectionEvents(args) {
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// added while the next section is still starting binds when it
|
|
937
|
-
// becomes active.
|
|
960
|
+
// The listener binds to whichever controller is currently active and
|
|
961
|
+
// re-binds on every `getOrCreateSectionController` /
|
|
962
|
+
// `disposeSectionController` transition, with snapshot replay on each
|
|
963
|
+
// migration. A listener added while the next section is still
|
|
964
|
+
// starting binds when it becomes active.
|
|
938
965
|
if (this.activeCohortMapKey === null &&
|
|
939
966
|
this.latestRequestedActiveCohortMapKey === null) {
|
|
940
967
|
throw new Error("[ToolkitCoordinator] subscribeSectionEvents requires an active section cohort; call getOrCreateSectionController first.");
|
|
@@ -973,7 +1000,7 @@ export class ToolkitCoordinator {
|
|
|
973
1000
|
};
|
|
974
1001
|
}
|
|
975
1002
|
/**
|
|
976
|
-
* Bind
|
|
1003
|
+
* Bind an active-cohort subscription to a section controller. Detaches any
|
|
977
1004
|
* prior controller binding the subscription was holding, attaches a
|
|
978
1005
|
* fresh `controller.subscribe(...)` callback, and replays the
|
|
979
1006
|
* canonical late-subscribe sequence (content-loaded × N then
|
|
@@ -1032,15 +1059,9 @@ export class ToolkitCoordinator {
|
|
|
1032
1059
|
const controller = this.sectionControllers.get(mapKey);
|
|
1033
1060
|
if (!controller)
|
|
1034
1061
|
return;
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
// `activeSubscriptions`. Map iteration yields keys inserted
|
|
1039
|
-
// during the loop; without snapshotting, the inner subscribe path
|
|
1040
|
-
// would bind the new listener once (correctly, with replay) and
|
|
1041
|
-
// the outer loop would then visit it a second time, double-
|
|
1042
|
-
// replaying the snapshot. Snapshot keeps "replay once per cohort
|
|
1043
|
-
// transition" intact.
|
|
1062
|
+
// Iterate a snapshot: a listener may call `subscribeSectionEvents` from
|
|
1063
|
+
// inside its replay delivery, and live Map iteration would then visit the
|
|
1064
|
+
// new entry and replay it a second time.
|
|
1044
1065
|
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
1045
1066
|
this.bindSubscriptionToController(sub, controller);
|
|
1046
1067
|
}
|
|
@@ -1056,10 +1077,7 @@ export class ToolkitCoordinator {
|
|
|
1056
1077
|
if (this.activeCohortMapKey === mapKey)
|
|
1057
1078
|
return;
|
|
1058
1079
|
this.activeCohortMapKey = null;
|
|
1059
|
-
|
|
1060
|
-
sub.unsubscribeCurrent?.();
|
|
1061
|
-
sub.unsubscribeCurrent = null;
|
|
1062
|
-
}
|
|
1080
|
+
this.detachActiveSubscriptions();
|
|
1063
1081
|
}
|
|
1064
1082
|
/**
|
|
1065
1083
|
* Clear the active cohort if `mapKey` matches it, detaching every
|
|
@@ -1071,10 +1089,14 @@ export class ToolkitCoordinator {
|
|
|
1071
1089
|
if (this.activeCohortMapKey !== mapKey)
|
|
1072
1090
|
return;
|
|
1073
1091
|
this.activeCohortMapKey = null;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1092
|
+
this.detachActiveSubscriptions();
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Detach every active subscription from its controller. Iterates a snapshot,
|
|
1096
|
+
* as `setActiveCohort` does, because a disposer may mutate
|
|
1097
|
+
* `activeSubscriptions`.
|
|
1098
|
+
*/
|
|
1099
|
+
detachActiveSubscriptions() {
|
|
1078
1100
|
for (const sub of Array.from(this.activeSubscriptions.values())) {
|
|
1079
1101
|
sub.unsubscribeCurrent?.();
|
|
1080
1102
|
sub.unsubscribeCurrent = null;
|
|
@@ -1084,20 +1106,14 @@ export class ToolkitCoordinator {
|
|
|
1084
1106
|
const eventTypeFilter = args.eventTypes ? new Set(args.eventTypes) : null;
|
|
1085
1107
|
const itemIdFilter = args.itemIds ? new Set(args.itemIds) : null;
|
|
1086
1108
|
return (event) => {
|
|
1087
|
-
if (eventTypeFilter
|
|
1088
|
-
const eventType = event?.type
|
|
1089
|
-
if (eventTypeFilter
|
|
1090
|
-
(!eventType || !eventTypeFilter.has(eventType))) {
|
|
1109
|
+
if (eventTypeFilter) {
|
|
1110
|
+
const eventType = event?.type;
|
|
1111
|
+
if (!eventType || !eventTypeFilter.has(eventType))
|
|
1091
1112
|
return false;
|
|
1092
|
-
}
|
|
1093
|
-
if (itemIdFilter) {
|
|
1094
|
-
const hasMatchingItem = Array.from(this.collectEventItemIds(event)).some((itemId) => itemIdFilter.has(itemId));
|
|
1095
|
-
if (!hasMatchingItem) {
|
|
1096
|
-
return false;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
1113
|
}
|
|
1100
|
-
|
|
1114
|
+
if (!itemIdFilter)
|
|
1115
|
+
return true;
|
|
1116
|
+
return Array.from(this.collectEventItemIds(event)).some((itemId) => itemIdFilter.has(itemId));
|
|
1101
1117
|
};
|
|
1102
1118
|
}
|
|
1103
1119
|
collectEventItemIds(event) {
|
|
@@ -1223,11 +1239,7 @@ export class ToolkitCoordinator {
|
|
|
1223
1239
|
if (this.disposePromise !== null) {
|
|
1224
1240
|
throw new ToolkitCoordinatorDisposedError();
|
|
1225
1241
|
}
|
|
1226
|
-
const key =
|
|
1227
|
-
assessmentId: this.assessmentId,
|
|
1228
|
-
sectionId: args.sectionId,
|
|
1229
|
-
attemptId: args.attemptId,
|
|
1230
|
-
};
|
|
1242
|
+
const key = this.sectionControllerKey(args);
|
|
1231
1243
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
1232
1244
|
this.latestRequestedActiveCohortMapKey = mapKey;
|
|
1233
1245
|
this.suspendActiveCohortIfSuperseded(mapKey);
|
|
@@ -1316,8 +1328,7 @@ export class ToolkitCoordinator {
|
|
|
1316
1328
|
if (this.sectionControllers.get(args.mapKey) !== existingController) {
|
|
1317
1329
|
return undefined;
|
|
1318
1330
|
}
|
|
1319
|
-
//
|
|
1320
|
-
// resolves to a previously-created controller still represents a
|
|
1331
|
+
// A `getOrCreateSectionController` call that resolves to a previously-created controller still represents a
|
|
1321
1332
|
// cohort transition from the toolkit's perspective (same-cohort
|
|
1322
1333
|
// re-entry is a no-op inside `setActiveCohort`). Active
|
|
1323
1334
|
// subscriptions migrate here so a host that subscribed once on
|
|
@@ -1457,8 +1468,7 @@ export class ToolkitCoordinator {
|
|
|
1457
1468
|
this.sectionControllers.set(args.mapKey, args.controller);
|
|
1458
1469
|
this.sectionControllerKeys.set(args.mapKey, args.key);
|
|
1459
1470
|
args.token.candidateClaimed = true;
|
|
1460
|
-
//
|
|
1461
|
-
// active cohort. Active subscriptions migrate to it before the
|
|
1471
|
+
// A freshly-resolved controller becomes the active cohort. Active subscriptions migrate to it before the
|
|
1462
1472
|
// `ready` lifecycle event and `onSectionControllerReady` hook
|
|
1463
1473
|
// fire so any synchronous post-ready work observes a coherent
|
|
1464
1474
|
// active-cohort view.
|
|
@@ -1489,7 +1499,7 @@ export class ToolkitCoordinator {
|
|
|
1489
1499
|
attemptId: args.attemptId,
|
|
1490
1500
|
},
|
|
1491
1501
|
recoverable: false,
|
|
1492
|
-
},
|
|
1502
|
+
}, { sectionId: args.sectionId, attemptId: args.attemptId });
|
|
1493
1503
|
}
|
|
1494
1504
|
/**
|
|
1495
1505
|
* A section that fails to start has no controller of its own to emit its
|
|
@@ -1517,11 +1527,7 @@ export class ToolkitCoordinator {
|
|
|
1517
1527
|
}
|
|
1518
1528
|
}
|
|
1519
1529
|
disposeSectionController(args) {
|
|
1520
|
-
const key =
|
|
1521
|
-
assessmentId: this.assessmentId,
|
|
1522
|
-
sectionId: args.sectionId,
|
|
1523
|
-
attemptId: args.attemptId,
|
|
1524
|
-
};
|
|
1530
|
+
const key = this.sectionControllerKey(args);
|
|
1525
1531
|
const mapKey = this.getSectionControllerMapKey(key);
|
|
1526
1532
|
const initEntry = this.sectionControllerInitEntries.get(mapKey);
|
|
1527
1533
|
if (initEntry) {
|
|
@@ -1538,8 +1544,7 @@ export class ToolkitCoordinator {
|
|
|
1538
1544
|
return this.trackSectionControllerDisposal(mapKey, retirementBarrier);
|
|
1539
1545
|
}
|
|
1540
1546
|
const persistenceStrategy = this.sectionPersistenceStrategies.get(mapKey);
|
|
1541
|
-
//
|
|
1542
|
-
// detach all listener-controller bindings before the controller
|
|
1547
|
+
// If the disposing cohort is the active one, detach all listener-controller bindings before the controller
|
|
1543
1548
|
// itself disposes. The subscription registry stays intact so a
|
|
1544
1549
|
// later `getOrCreateSectionController(...)` re-binds the same
|
|
1545
1550
|
// listeners to the new controller.
|
|
@@ -1704,14 +1709,13 @@ export class ToolkitCoordinator {
|
|
|
1704
1709
|
await cleanup(() => this.ttsService.dispose());
|
|
1705
1710
|
await cleanup(() => this.toolProviderRegistry.destroy());
|
|
1706
1711
|
await cleanup(() => this.highlightCoordinator.destroy());
|
|
1707
|
-
|
|
1708
|
-
await cleanup(() => this.toolCoordinator.releaseTool(toolId));
|
|
1709
|
-
}
|
|
1712
|
+
await cleanup(() => this.ownedToolCoordinator.destroy());
|
|
1710
1713
|
await cleanup(() => this.catalogResolver.destroy());
|
|
1711
1714
|
await cleanup(() => this.toolRequests.dispose());
|
|
1712
1715
|
await cleanup(() => this.policyEngine.dispose());
|
|
1713
1716
|
this.toolContextResolvers.clear();
|
|
1714
1717
|
this.toolContextResolverChangeListeners.clear();
|
|
1718
|
+
this.policyDiagnosticListeners.clear();
|
|
1715
1719
|
this.sectionControllerLifecycleListeners.clear();
|
|
1716
1720
|
this.telemetryListeners.clear();
|
|
1717
1721
|
this.frameworkErrorHookUnsubscribe?.();
|
|
@@ -1741,9 +1745,9 @@ export class ToolkitCoordinator {
|
|
|
1741
1745
|
await Promise.allSettled(pending);
|
|
1742
1746
|
}
|
|
1743
1747
|
/**
|
|
1744
|
-
* Initialize TTS service
|
|
1748
|
+
* Initialize the TTS service from the `textToSpeech` tool config.
|
|
1745
1749
|
*/
|
|
1746
|
-
async ensureTTSReady(
|
|
1750
|
+
async ensureTTSReady() {
|
|
1747
1751
|
this.assertNotDisposed();
|
|
1748
1752
|
await this.waitForPendingTTSReconfigure();
|
|
1749
1753
|
this.assertNotDisposed();
|
|
@@ -1751,7 +1755,7 @@ export class ToolkitCoordinator {
|
|
|
1751
1755
|
return;
|
|
1752
1756
|
if (this.ttsInitPromise)
|
|
1753
1757
|
return this.ttsInitPromise;
|
|
1754
|
-
this.ttsInitPromise = this._initializeTTS(
|
|
1758
|
+
this.ttsInitPromise = this._initializeTTS()
|
|
1755
1759
|
.then(() => this.assertNotDisposed())
|
|
1756
1760
|
.finally(() => {
|
|
1757
1761
|
this.ttsInitPromise = undefined;
|
|
@@ -1766,10 +1770,10 @@ export class ToolkitCoordinator {
|
|
|
1766
1770
|
pending = this.ttsReconfigurePromise;
|
|
1767
1771
|
}
|
|
1768
1772
|
}
|
|
1769
|
-
async _initializeTTS(
|
|
1773
|
+
async _initializeTTS() {
|
|
1770
1774
|
if (this.ttsInitialized)
|
|
1771
1775
|
return;
|
|
1772
|
-
const resolvedToolConfig = this.resolveTTSToolConfig(
|
|
1776
|
+
const resolvedToolConfig = this.resolveTTSToolConfig();
|
|
1773
1777
|
const runtimeSettings = resolveTTSRuntimeSettings(resolvedToolConfig);
|
|
1774
1778
|
const resolvedBackend = resolveTTSBackend(runtimeSettings);
|
|
1775
1779
|
const runtimeTTSConfig = buildRuntimeTTSConfig(runtimeSettings);
|
|
@@ -1788,7 +1792,6 @@ export class ToolkitCoordinator {
|
|
|
1788
1792
|
operation: "tts-init",
|
|
1789
1793
|
backend: resolvedBackend,
|
|
1790
1794
|
});
|
|
1791
|
-
// Try to use TTS provider from registry if available
|
|
1792
1795
|
if (this.toolProviderRegistry.has("textToSpeech")) {
|
|
1793
1796
|
try {
|
|
1794
1797
|
// Browser speech follows a server provider that fails to start.
|
|
@@ -1845,7 +1848,7 @@ export class ToolkitCoordinator {
|
|
|
1845
1848
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
1846
1849
|
throw error;
|
|
1847
1850
|
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1848
|
-
this.
|
|
1851
|
+
this.reportStartFailure(normalized, { phase: "tts-init" }, ["textToSpeech"]);
|
|
1849
1852
|
await this.emitTelemetry("pie-toolkit-tts-init-error", {
|
|
1850
1853
|
message: normalized.message,
|
|
1851
1854
|
});
|
|
@@ -1859,8 +1862,8 @@ export class ToolkitCoordinator {
|
|
|
1859
1862
|
throw normalized;
|
|
1860
1863
|
}
|
|
1861
1864
|
}
|
|
1862
|
-
resolveTTSToolConfig(
|
|
1863
|
-
return
|
|
1865
|
+
resolveTTSToolConfig() {
|
|
1866
|
+
return this.getTTSConfigFromProviders() || {};
|
|
1864
1867
|
}
|
|
1865
1868
|
async initializeTTSService(provider, config) {
|
|
1866
1869
|
const nextProviderOptions = {
|
|
@@ -1933,7 +1936,7 @@ export class ToolkitCoordinator {
|
|
|
1933
1936
|
*/
|
|
1934
1937
|
async startTTSForReadiness() {
|
|
1935
1938
|
try {
|
|
1936
|
-
await this.ensureTTSReady(
|
|
1939
|
+
await this.ensureTTSReady();
|
|
1937
1940
|
}
|
|
1938
1941
|
catch (error) {
|
|
1939
1942
|
if (error instanceof ToolkitCoordinatorDisposedError)
|
|
@@ -1951,13 +1954,11 @@ export class ToolkitCoordinator {
|
|
|
1951
1954
|
if (typeof toolId !== "string" || toolId.trim().length === 0) {
|
|
1952
1955
|
throw new Error("Tool id must be a non-empty string.");
|
|
1953
1956
|
}
|
|
1954
|
-
// An empty registry means the host supplied none,
|
|
1955
|
-
//
|
|
1956
|
-
//
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
// Validation reports the missing registry once per coordinator, as
|
|
1960
|
-
// `tools.registryUnavailable`; a report per call would be noise.
|
|
1957
|
+
// An empty registry means the host supplied none, so there is nothing to
|
|
1958
|
+
// check an id against. Throwing here would fail every tool-config call,
|
|
1959
|
+
// the default providers' own included, and once left a host without a
|
|
1960
|
+
// registry unable to read its config. Validation reports the missing
|
|
1961
|
+
// registry once per coordinator, as `tools.registryUnavailable`.
|
|
1961
1962
|
if (this.toolRegistry.getAllToolIds().length === 0)
|
|
1962
1963
|
return;
|
|
1963
1964
|
if (!this.toolRegistry.get(toolId)) {
|
|
@@ -2002,7 +2003,6 @@ export class ToolkitCoordinator {
|
|
|
2002
2003
|
return this.config.tools?.providers?.[toolId] || null;
|
|
2003
2004
|
}
|
|
2004
2005
|
updateToolConfig(toolId, updates) {
|
|
2005
|
-
// Update config
|
|
2006
2006
|
this.assertCanonicalToolId(toolId);
|
|
2007
2007
|
const current = this.getToolConfig(toolId) || {};
|
|
2008
2008
|
if (!this.config.tools) {
|
|
@@ -2016,10 +2016,10 @@ export class ToolkitCoordinator {
|
|
|
2016
2016
|
this.config.tools.providers = {};
|
|
2017
2017
|
}
|
|
2018
2018
|
const nextProviders = {
|
|
2019
|
-
...
|
|
2019
|
+
...this.config.tools.providers,
|
|
2020
2020
|
[toolId]: mergeToolConfigUpdate(toolId, current, updates),
|
|
2021
2021
|
};
|
|
2022
|
-
|
|
2022
|
+
const validated = this.validateToolsConfig({
|
|
2023
2023
|
...this.config.tools,
|
|
2024
2024
|
providers: nextProviders,
|
|
2025
2025
|
}, {
|
|
@@ -2027,13 +2027,11 @@ export class ToolkitCoordinator {
|
|
|
2027
2027
|
source: "ToolkitCoordinator.updateToolConfig",
|
|
2028
2028
|
toolRegistry: this.toolRegistry,
|
|
2029
2029
|
});
|
|
2030
|
+
this.config.tools = validated;
|
|
2030
2031
|
// The engine's tools input follows the validated config; its `inputs`
|
|
2031
2032
|
// change event is what makes toolbars re-decide.
|
|
2032
|
-
this.policyEngine.updateInputs({
|
|
2033
|
-
tools: this.config.tools,
|
|
2034
|
-
});
|
|
2033
|
+
this.policyEngine.updateInputs({ tools: validated });
|
|
2035
2034
|
void this.emitTelemetry("pie-toolkit-tool-config-updated", { toolId });
|
|
2036
|
-
// Apply configuration changes to services
|
|
2037
2035
|
this._applyToolConfigChange(toolId);
|
|
2038
2036
|
}
|
|
2039
2037
|
/**
|
|
@@ -2072,9 +2070,7 @@ export class ToolkitCoordinator {
|
|
|
2072
2070
|
});
|
|
2073
2071
|
validated.placement = nextPlacement;
|
|
2074
2072
|
this.config.tools = validated;
|
|
2075
|
-
this.policyEngine.updateInputs({
|
|
2076
|
-
tools: this.config.tools,
|
|
2077
|
-
});
|
|
2073
|
+
this.policyEngine.updateInputs({ tools: validated });
|
|
2078
2074
|
}
|
|
2079
2075
|
/**
|
|
2080
2076
|
* Resolve the visible tool set for a given placement level + scope.
|
|
@@ -2088,9 +2084,11 @@ export class ToolkitCoordinator {
|
|
|
2088
2084
|
return decision;
|
|
2089
2085
|
}
|
|
2090
2086
|
/**
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2087
|
+
* Report each policy diagnostic as a console warning and to the
|
|
2088
|
+
* {@link onPolicyDiagnostic} listeners, once per code and tool id, and per
|
|
2089
|
+
* item for `tool-policy.itemSettingNotApplied`. Every toolbar and feature
|
|
2090
|
+
* asks for decisions, so a conflict would otherwise repeat on each; the
|
|
2091
|
+
* engine's input diagnostics arrive with each policy change.
|
|
2094
2092
|
*/
|
|
2095
2093
|
warnPolicyDiagnostics(diagnostics) {
|
|
2096
2094
|
for (const diagnostic of diagnostics) {
|
|
@@ -2098,10 +2096,21 @@ export class ToolkitCoordinator {
|
|
|
2098
2096
|
? diagnostic.details.itemId
|
|
2099
2097
|
: "";
|
|
2100
2098
|
const key = `${diagnostic.code}\0${diagnostic.toolId}\0${itemId}`;
|
|
2101
|
-
if (this.
|
|
2099
|
+
if (this.reportedPolicyDiagnostics.has(key))
|
|
2102
2100
|
continue;
|
|
2103
|
-
this.
|
|
2101
|
+
this.reportedPolicyDiagnostics.set(key, diagnostic);
|
|
2104
2102
|
console.warn(`[ToolkitCoordinator] ${diagnostic.message}`);
|
|
2103
|
+
for (const listener of Array.from(this.policyDiagnosticListeners)) {
|
|
2104
|
+
this.notifyPolicyDiagnosticListener(listener, diagnostic);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
notifyPolicyDiagnosticListener(listener, diagnostic) {
|
|
2109
|
+
try {
|
|
2110
|
+
listener(diagnostic);
|
|
2111
|
+
}
|
|
2112
|
+
catch (error) {
|
|
2113
|
+
console.error("[ToolkitCoordinator] Policy diagnostic listener failed:", error);
|
|
2105
2114
|
}
|
|
2106
2115
|
}
|
|
2107
2116
|
/**
|
|
@@ -2123,8 +2132,8 @@ export class ToolkitCoordinator {
|
|
|
2123
2132
|
* `pnpEnforcement` is not consulted.
|
|
2124
2133
|
*
|
|
2125
2134
|
* Reports once per coordinator when it is asked about a feature with no
|
|
2126
|
-
* assessment bound. A host in that state gets a
|
|
2127
|
-
*
|
|
2135
|
+
* assessment bound. A host in that state gets a denial for every capability
|
|
2136
|
+
* no item requires, which is indistinguishable from a student who was
|
|
2128
2137
|
* properly declined — so without this, forgetting {@link updateAssessment}
|
|
2129
2138
|
* presents as an accommodation that silently never appears.
|
|
2130
2139
|
*
|
|
@@ -2139,13 +2148,11 @@ export class ToolkitCoordinator {
|
|
|
2139
2148
|
decideFeaturePolicy(featureId, scope) {
|
|
2140
2149
|
const decision = this.policyEngine.decideFeature(featureId, scope);
|
|
2141
2150
|
this.warnPolicyDiagnostics(decision.diagnostics);
|
|
2142
|
-
const unboundIsMisconfigured = this.config.assessmentOptional !== true ||
|
|
2143
|
-
this.pnpEnforcementOverride === "on";
|
|
2144
2151
|
if (!decision.assessmentBound &&
|
|
2145
|
-
|
|
2152
|
+
this.policyEngine.getInputs().assessmentExpected &&
|
|
2146
2153
|
!this.reportedUnboundFeaturePolicy) {
|
|
2147
2154
|
this.reportedUnboundFeaturePolicy = true;
|
|
2148
|
-
console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so
|
|
2155
|
+
console.warn(`[ToolkitCoordinator] Feature policy was asked about "${featureId}" with no assessment bound, so profile, district and test-administration policy have nothing to read and only an item's requiredTools can grant. Pass the assessment to the toolkit's assessment property, or call updateAssessment(...), before relying on any accommodation. Reported once per coordinator.`);
|
|
2149
2156
|
}
|
|
2150
2157
|
return decision;
|
|
2151
2158
|
}
|
|
@@ -2169,6 +2176,15 @@ export class ToolkitCoordinator {
|
|
|
2169
2176
|
onPolicyChange(listener) {
|
|
2170
2177
|
return this.policyEngine.onPolicyChange(listener);
|
|
2171
2178
|
}
|
|
2179
|
+
onPolicyDiagnostic(listener) {
|
|
2180
|
+
this.policyDiagnosticListeners.add(listener);
|
|
2181
|
+
for (const diagnostic of Array.from(this.reportedPolicyDiagnostics.values())) {
|
|
2182
|
+
this.notifyPolicyDiagnosticListener(listener, diagnostic);
|
|
2183
|
+
}
|
|
2184
|
+
return () => {
|
|
2185
|
+
this.policyDiagnosticListeners.delete(listener);
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2172
2188
|
/**
|
|
2173
2189
|
* Subscribe to accessibility-catalog registrations and removals.
|
|
2174
2190
|
*
|
|
@@ -2200,10 +2216,10 @@ export class ToolkitCoordinator {
|
|
|
2200
2216
|
}
|
|
2201
2217
|
/**
|
|
2202
2218
|
* Override the auto-mode PNP/profile enforcement decision; `null` returns
|
|
2203
|
-
* to auto-mode.
|
|
2219
|
+
* to auto-mode. `"on"` and `"off"` stick across assessment swaps until
|
|
2220
|
+
* cleared.
|
|
2204
2221
|
*/
|
|
2205
2222
|
setPnpEnforcement(mode) {
|
|
2206
|
-
this.pnpEnforcementOverride = mode;
|
|
2207
2223
|
this.policyEngine.updateInputs({ pnpEnforcement: mode });
|
|
2208
2224
|
}
|
|
2209
2225
|
/**
|
|
@@ -2314,13 +2330,9 @@ export class ToolkitCoordinator {
|
|
|
2314
2330
|
return this.toolRequests.onTargetsChange(listener);
|
|
2315
2331
|
}
|
|
2316
2332
|
resolveConfiguredPnpEnforcement() {
|
|
2317
|
-
|
|
2318
|
-
return tools?.pnpEnforcement ?? null;
|
|
2333
|
+
return this.config.tools?.pnpEnforcement ?? null;
|
|
2319
2334
|
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Apply tool configuration changes to underlying services.
|
|
2322
|
-
* Called after updateToolConfig().
|
|
2323
|
-
*/
|
|
2335
|
+
/** Apply an `updateToolConfig` change to the underlying services. */
|
|
2324
2336
|
_applyToolConfigChange(toolId) {
|
|
2325
2337
|
if (this.disposePromise !== null)
|
|
2326
2338
|
return;
|