@pie-players/pie-assessment-toolkit 0.3.63 → 0.3.65
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 +92 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-cckwpz6c.js +51 -0
- package/dist/components/chunks/ItemToolBar-pryf0rtz.js +22 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.js +12 -4
- package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
- package/dist/policy/core/ToolPolicyEngine.js +27 -0
- package/dist/policy/core/feature-decision.d.ts +57 -0
- package/dist/policy/core/feature-decision.js +40 -0
- package/dist/policy/engine.d.ts +1 -0
- package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
- package/dist/policy/sources/PnpPolicySource.js +41 -11
- package/dist/runtime/catalog-registration.d.ts +56 -1
- package/dist/runtime/catalog-registration.js +64 -31
- package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
- package/dist/runtime/composition-emit-scheduler.js +154 -0
- package/dist/runtime/core/engine-resolver.d.ts +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
- package/dist/services/AccessibilityCatalogResolver.js +183 -58
- package/dist/services/SSMLExtractor.js +28 -18
- package/dist/services/TTSService.d.ts +25 -0
- package/dist/services/TTSService.js +241 -45
- package/dist/services/ToolRegistry.d.ts +218 -8
- package/dist/services/ToolRegistry.js +124 -8
- package/dist/services/ToolkitCoordinator.d.ts +25 -3
- package/dist/services/ToolkitCoordinator.js +47 -6
- package/dist/services/catalog-media.d.ts +25 -0
- package/dist/services/catalog-media.js +101 -0
- package/dist/services/createDefaultToolRegistry.d.ts +25 -58
- package/dist/services/createDefaultToolRegistry.js +24 -104
- package/dist/services/defaultPersonalNeedsProfile.d.ts +20 -2
- package/dist/services/defaultPersonalNeedsProfile.js +21 -19
- package/dist/services/interfaces.d.ts +29 -2
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/spoken-audio-cards.d.ts +54 -0
- package/dist/services/spoken-audio-cards.js +66 -0
- package/dist/services/tool-config-defaults.d.ts +7 -23
- package/dist/services/tool-config-defaults.js +7 -46
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +44 -4
- package/dist/services/tts/browser-provider.js +2 -1
- package/dist/services/tts/math-aware-text-processing.js +3 -3
- package/dist/services/tts/text-processing.d.ts +51 -0
- package/dist/services/tts/text-processing.js +117 -1
- package/dist/services/tts-runtime-config.js +7 -2
- package/dist/tools/internal.d.ts +34 -0
- package/dist/tools/internal.js +33 -0
- package/dist/tools/tool-tag-map.d.ts +15 -3
- package/dist/tools/tool-tag-map.js +21 -18
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-843902tp.js +0 -51
- package/dist/components/chunks/ItemToolBar-84nv78dy.js +0 -22
- package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
- package/dist/tools/registrations/accessibility-tools.js +0 -217
- package/dist/tools/registrations/calculator.d.ts +0 -20
- package/dist/tools/registrations/calculator.js +0 -228
- package/dist/tools/registrations/interaction-tools.d.ts +0 -27
- package/dist/tools/registrations/interaction-tools.js +0 -143
- package/dist/tools/registrations/measurement-tools.d.ts +0 -24
- package/dist/tools/registrations/measurement-tools.js +0 -130
- package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
- package/dist/tools/registrations/subject-specific-tools.js +0 -158
- package/dist/tools/registrations/tts.d.ts +0 -21
- package/dist/tools/registrations/tts.js +0 -184
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Central registry for all assessment tools. Manages tool metadata, visibility logic,
|
|
5
5
|
* and button/instance creation. Supports dynamic registration and override by integrators.
|
|
6
6
|
*/
|
|
7
|
+
import type { ItemEntity } from "@pie-players/pie-players-shared/types";
|
|
8
|
+
import type { CatalogOwnerContext } from "./AccessibilityCatalogResolver.js";
|
|
7
9
|
import type { ToolContext, ToolLevel } from "./tool-context.js";
|
|
8
10
|
import type { ToolComponentOverrides } from "../tools/tool-tag-map.js";
|
|
9
|
-
import type { ElementToolStateStoreApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi } from "./interfaces.js";
|
|
11
|
+
import type { AccessibilityCatalogResolverApi, ElementToolStateStoreApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi } from "./interfaces.js";
|
|
10
12
|
import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
11
13
|
import type { ToolProviderConfig as ToolRuntimeConfig } from "./tools-config-normalizer.js";
|
|
12
14
|
import type { ToolConfigDiagnostic } from "./tool-config-validation.js";
|
|
@@ -14,7 +16,14 @@ export type ToolModuleLoader = () => Promise<unknown>;
|
|
|
14
16
|
export interface ToolToolbarButtonDefinition {
|
|
15
17
|
toolId: string;
|
|
16
18
|
label: string;
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Optional to match what the renderers already do: `ToolButton.svelte` and
|
|
21
|
+
* `ItemToolBar.svelte` both guard on `button.icon`, and `ToolbarItem.icon` is
|
|
22
|
+
* already optional, so requiring it here claimed a guarantee nothing relied
|
|
23
|
+
* on. A registration that renders a button still has to declare an icon —
|
|
24
|
+
* `assertToolRegistrationShape` enforces that.
|
|
25
|
+
*/
|
|
26
|
+
icon?: string;
|
|
18
27
|
ariaLabel: string;
|
|
19
28
|
tooltip?: string;
|
|
20
29
|
onClick: () => void;
|
|
@@ -148,8 +157,135 @@ export interface ToolToolbarRenderResult {
|
|
|
148
157
|
sync?: () => void;
|
|
149
158
|
subscribeActive?: (callback: (active: boolean) => void) => () => void;
|
|
150
159
|
}
|
|
151
|
-
export type ToolActivation = "toolbar-toggle" | "selection-gateway";
|
|
160
|
+
export type ToolActivation = "toolbar-toggle" | "selection-gateway" | "region";
|
|
152
161
|
export type ToolSingletonScope = "section";
|
|
162
|
+
/**
|
|
163
|
+
* Services a host hands a capability rendering into one of its surfaces.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately the same three references a toolbar tool reaches through
|
|
166
|
+
* `ToolbarContext`, and no more: a capability that needs the coordinator can ask
|
|
167
|
+
* it for anything else. Passing the host's own component or state would make the
|
|
168
|
+
* registration depend on which renderer mounted it.
|
|
169
|
+
*/
|
|
170
|
+
export interface ToolSurfaceServices {
|
|
171
|
+
toolkitCoordinator: ToolkitCoordinatorApi | null;
|
|
172
|
+
ttsService: TtsServiceApi | null;
|
|
173
|
+
catalogResolver: AccessibilityCatalogResolverApi | null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* What a host tells a capability when asking it to fill a surface.
|
|
177
|
+
*
|
|
178
|
+
* `surface` is a host-defined slot name. Core defines none and validates only
|
|
179
|
+
* that a region capability claims at least one, so a host can open a new surface
|
|
180
|
+
* without a change here and a capability can declare which of a host's surfaces
|
|
181
|
+
* it fits. Section-player ships `"content-media"` (the media region on an item
|
|
182
|
+
* or passage card) and `"section-overlay"` (the section-scoped singleton).
|
|
183
|
+
*
|
|
184
|
+
* `content` carries whatever the capability's own `requiresAuthoredContent`
|
|
185
|
+
* resolved, so the host neither inspects nor names it — it hands back what the
|
|
186
|
+
* capability asked for.
|
|
187
|
+
*/
|
|
188
|
+
export interface ToolSurfaceRenderContext {
|
|
189
|
+
toolId: string;
|
|
190
|
+
/** The PNP/AfA support id policy granted for this render. */
|
|
191
|
+
featureId: string;
|
|
192
|
+
/** Host slot being filled. */
|
|
193
|
+
surface: string;
|
|
194
|
+
/** Feature parameters from the policy decision, if any. */
|
|
195
|
+
parameters?: unknown;
|
|
196
|
+
/** Resolved content dependency, when the capability declares one. */
|
|
197
|
+
content?: unknown;
|
|
198
|
+
services: ToolSurfaceServices;
|
|
199
|
+
componentOverrides?: ToolComponentOverrides;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* What a host tells a capability when asking whether the content it needs is
|
|
203
|
+
* present.
|
|
204
|
+
*/
|
|
205
|
+
export interface ToolContentDependencyContext {
|
|
206
|
+
/** The PNP/AfA support id being resolved. */
|
|
207
|
+
featureId: string;
|
|
208
|
+
/** Feature parameters from the policy decision, if any. */
|
|
209
|
+
parameters?: unknown;
|
|
210
|
+
catalogResolver: AccessibilityCatalogResolverApi | null;
|
|
211
|
+
/** Owner scope for catalog lookups, without `modelId`. */
|
|
212
|
+
ownerContext: CatalogOwnerContext;
|
|
213
|
+
/** The item in scope, when the host renders per item. */
|
|
214
|
+
item?: ItemEntity | null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A capability's declaration that it needs authored content to have anything to
|
|
218
|
+
* show, and the check that decides whether that content is present.
|
|
219
|
+
*
|
|
220
|
+
* This is the resource half of AfA's PNP/DRD pair. Signing needs an authored
|
|
221
|
+
* catalog card, braille a transcription, authored SSML a `<speak>` in that item.
|
|
222
|
+
* It is intrinsic to the capability, unlike eligibility tier, which is a property
|
|
223
|
+
* of the program.
|
|
224
|
+
*
|
|
225
|
+
* Two independent things follow from declaring it, and both used to be done by
|
|
226
|
+
* naming ids in core:
|
|
227
|
+
*
|
|
228
|
+
* 1. **Availability is grant AND content.** The host renders only when policy
|
|
229
|
+
* granted the feature *and* `resolve` returned something. Neither half
|
|
230
|
+
* implies the other and neither is a default, so a learner with the
|
|
231
|
+
* accommodation still sees nothing on an item that carries no resource — no
|
|
232
|
+
* dead affordance.
|
|
233
|
+
* 2. **It is not granted wholesale.** A host building a default grant list
|
|
234
|
+
* filters on this declaration instead of on a compile-time array of ids it
|
|
235
|
+
* cannot extend. `@pie-players/pie-default-tool-loaders` asserts its
|
|
236
|
+
* universal preset holds no id belonging to a capability that declares one.
|
|
237
|
+
*
|
|
238
|
+
* `resolve` returns the resolved content, which the host hands straight back
|
|
239
|
+
* through `ToolSurfaceRenderContext.content` without inspecting it. That is what
|
|
240
|
+
* keeps the resolver and the host from knowing which accommodation they are
|
|
241
|
+
* resolving.
|
|
242
|
+
*
|
|
243
|
+
* Resolvable only on a surface the host renders per item or per passage:
|
|
244
|
+
* `ownerContext` names an item model or a passage, never a section, because a DRD
|
|
245
|
+
* resource pairs with a piece of content and not with a container. A capability
|
|
246
|
+
* declaring one and claiming a section-scoped surface is declined there rather
|
|
247
|
+
* than mounted with no content. `resolve` is also synchronous — a capability whose
|
|
248
|
+
* resource has to be fetched resolves the reference here and fetches inside its
|
|
249
|
+
* own element, where it can show its own pending state.
|
|
250
|
+
*/
|
|
251
|
+
export interface ToolContentDependency {
|
|
252
|
+
/**
|
|
253
|
+
* The resolved content, or `null` when the item carries none.
|
|
254
|
+
*
|
|
255
|
+
* Must be JSON-serializable. A host re-resolves on every policy and catalog
|
|
256
|
+
* signal and compares the answer structurally to decide whether anything moved,
|
|
257
|
+
* because every resolution builds fresh objects and identity would report a
|
|
258
|
+
* change each time. A `Map`, a function or a DOM node therefore compares equal
|
|
259
|
+
* to itself across a real change and the capability never hears about it; a
|
|
260
|
+
* cyclic value throws inside the host's own reconciliation.
|
|
261
|
+
*/
|
|
262
|
+
resolve(context: ToolContentDependencyContext): unknown | null;
|
|
263
|
+
/**
|
|
264
|
+
* Optional human-readable description of what has to be authored, for a
|
|
265
|
+
* policy debugger explaining why an otherwise-granted capability is absent.
|
|
266
|
+
*/
|
|
267
|
+
description?: string;
|
|
268
|
+
}
|
|
269
|
+
export interface ToolSurfaceRenderResult {
|
|
270
|
+
/** Element for the host to mount into its surface. */
|
|
271
|
+
element: HTMLElement;
|
|
272
|
+
/** Accessible name for the surface, when the capability owns that wording. */
|
|
273
|
+
ariaLabel?: string;
|
|
274
|
+
/**
|
|
275
|
+
* Reapply props after policy, parameters or content change.
|
|
276
|
+
*
|
|
277
|
+
* Takes the current context rather than closing over the one captured at
|
|
278
|
+
* render: the whole point of reconciling by `toolId` instead of remounting is
|
|
279
|
+
* that a re-resolve reaches the mounted element, and a closure over the
|
|
280
|
+
* render-time context re-applies the values the host already had. A signed
|
|
281
|
+
* alternate re-resolved to a different recording — a live `signLang` change, or
|
|
282
|
+
* a catalog registering after first paint — would otherwise leave the learner
|
|
283
|
+
* watching the previous one with no error anywhere.
|
|
284
|
+
*/
|
|
285
|
+
sync?: (context: ToolSurfaceRenderContext) => void;
|
|
286
|
+
/** Release listeners and media before the host unmounts the element. */
|
|
287
|
+
destroy?: () => void;
|
|
288
|
+
}
|
|
153
289
|
/**
|
|
154
290
|
* Tool registration interface
|
|
155
291
|
*/
|
|
@@ -160,16 +296,31 @@ export interface ToolRegistration {
|
|
|
160
296
|
name: string;
|
|
161
297
|
/** Description of what the tool does */
|
|
162
298
|
description: string;
|
|
163
|
-
/**
|
|
164
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Icon identifier or SVG string. Required for the activations that render a
|
|
301
|
+
* toolbar button; a region capability has no button, so it has no icon.
|
|
302
|
+
*/
|
|
303
|
+
icon?: string | ((context: ToolContext) => string);
|
|
165
304
|
/** Which levels this tool supports */
|
|
166
305
|
supportedLevels: ToolLevel[];
|
|
167
306
|
/**
|
|
168
307
|
* Activation model for this tool.
|
|
169
308
|
* - toolbar-toggle: rendered as a toolbar button (default)
|
|
170
309
|
* - selection-gateway: rendered as a singleton selection-driven gateway
|
|
310
|
+
* - region: rendered into a host surface, with no toolbar button
|
|
171
311
|
*/
|
|
172
312
|
activation?: ToolActivation;
|
|
313
|
+
/**
|
|
314
|
+
* Host surfaces this capability can fill. Required for `activation: "region"`
|
|
315
|
+
* and meaningful for any activation whose capability also has a non-toolbar
|
|
316
|
+
* surface — the annotation toolbar is both a toolbar button and a
|
|
317
|
+
* section-scoped singleton.
|
|
318
|
+
*
|
|
319
|
+
* Names are the host's, not core's. A host discovers what it can mount by
|
|
320
|
+
* asking {@link ToolRegistry.getToolsBySurface}, which is what keeps a
|
|
321
|
+
* renderer from naming a capability.
|
|
322
|
+
*/
|
|
323
|
+
surfaces?: string[];
|
|
173
324
|
/**
|
|
174
325
|
* Optional singleton scope for activation models that mount exactly one instance.
|
|
175
326
|
*/
|
|
@@ -180,6 +331,14 @@ export interface ToolRegistration {
|
|
|
180
331
|
* Example: ['calculator', 'basic-calculator', 'scientific-calculator']
|
|
181
332
|
*/
|
|
182
333
|
pnpSupportIds?: string[];
|
|
334
|
+
/**
|
|
335
|
+
* Authored content this capability needs before it has anything to show.
|
|
336
|
+
*
|
|
337
|
+
* Declaring it makes availability "grant AND content", and excludes the
|
|
338
|
+
* capability from any wholesale default grant. See
|
|
339
|
+
* {@link ToolContentDependency}.
|
|
340
|
+
*/
|
|
341
|
+
requiresAuthoredContent?: ToolContentDependency;
|
|
183
342
|
/**
|
|
184
343
|
* Optional provider registration metadata.
|
|
185
344
|
* When present, ToolkitCoordinator can register provider(s) generically
|
|
@@ -196,12 +355,31 @@ export interface ToolRegistration {
|
|
|
196
355
|
* Pass 2: Tool decides if it's relevant in this context
|
|
197
356
|
* Called ONLY if orchestrator has already allowed the tool (Pass 1)
|
|
198
357
|
*
|
|
358
|
+
* Required for the toolbar activations, and meaningless for `activation:
|
|
359
|
+
* "region"`: a region capability has no toolbar presence to be relevant to, and
|
|
360
|
+
* the question it *would* answer — is there anything to show here — is
|
|
361
|
+
* `requiresAuthoredContent`. A registration that omits this is never returned
|
|
362
|
+
* by `getVisibleTools`.
|
|
363
|
+
*
|
|
199
364
|
* @param context - Rich context about where tool is being evaluated
|
|
200
365
|
* @returns true if tool should be visible, false to hide
|
|
201
366
|
*/
|
|
202
|
-
isVisibleInContext(context: ToolContext): boolean;
|
|
203
|
-
/**
|
|
204
|
-
|
|
367
|
+
isVisibleInContext?(context: ToolContext): boolean;
|
|
368
|
+
/**
|
|
369
|
+
* Toolbar render contract. Required for `toolbar-toggle` and
|
|
370
|
+
* `selection-gateway`; a region capability renders through
|
|
371
|
+
* {@link ToolRegistration.renderSurface} instead.
|
|
372
|
+
*/
|
|
373
|
+
renderToolbar?(context: ToolContext, toolbarContext: ToolbarContext): ToolToolbarRenderResult | null;
|
|
374
|
+
/**
|
|
375
|
+
* Render into one of the host surfaces this capability declares.
|
|
376
|
+
*
|
|
377
|
+
* Returning `null` means "nothing to show for this render" and is not an
|
|
378
|
+
* error — a capability may decline once the host has already granted and
|
|
379
|
+
* resolved content. The host mounts the returned element and calls `sync()`
|
|
380
|
+
* when policy, parameters or content move.
|
|
381
|
+
*/
|
|
382
|
+
renderSurface?(context: ToolSurfaceRenderContext): ToolSurfaceRenderResult | null;
|
|
205
383
|
}
|
|
206
384
|
/**
|
|
207
385
|
* Tool Registry
|
|
@@ -290,6 +468,23 @@ export declare class ToolRegistry {
|
|
|
290
468
|
* Resolve singleton scope for a tool when present.
|
|
291
469
|
*/
|
|
292
470
|
getToolSingletonScope(toolId: string): ToolSingletonScope | null;
|
|
471
|
+
/**
|
|
472
|
+
* Registrations that can fill a named host surface.
|
|
473
|
+
*
|
|
474
|
+
* The discovery call a renderer makes instead of naming a capability. Order
|
|
475
|
+
* follows registration order, so a host mounting several capabilities into one
|
|
476
|
+
* surface gets a stable sequence without core deciding a precedence it has no
|
|
477
|
+
* basis for.
|
|
478
|
+
*/
|
|
479
|
+
getToolsBySurface(surface: string): ToolRegistration[];
|
|
480
|
+
/**
|
|
481
|
+
* Support ids belonging to capabilities that need authored content.
|
|
482
|
+
*
|
|
483
|
+
* What a host filters a default grant list on, in place of the compile-time
|
|
484
|
+
* exclusion array this replaced: granting one of these wholesale grants an
|
|
485
|
+
* accommodation to learners with no documented need for it.
|
|
486
|
+
*/
|
|
487
|
+
getContentDependentSupportIds(): string[];
|
|
293
488
|
/**
|
|
294
489
|
* Filter tool IDs by activation type.
|
|
295
490
|
*/
|
|
@@ -319,6 +514,9 @@ export declare class ToolRegistry {
|
|
|
319
514
|
supportedLevels: ToolLevel[];
|
|
320
515
|
activation: ToolActivation;
|
|
321
516
|
singletonScope: ToolSingletonScope | null;
|
|
517
|
+
surfaces: string[];
|
|
518
|
+
requiresAuthoredContent: boolean;
|
|
519
|
+
contentDependencyDescription: string | null;
|
|
322
520
|
}>;
|
|
323
521
|
/**
|
|
324
522
|
* Generate PNP support IDs from enabled tools
|
|
@@ -358,4 +556,16 @@ export declare class ToolRegistry {
|
|
|
358
556
|
* Render a tool for toolbar use with component overrides attached.
|
|
359
557
|
*/
|
|
360
558
|
renderForToolbar(toolId: string, context: ToolContext, toolbarContext: ToolbarContext): ToolToolbarRenderResult | null;
|
|
559
|
+
/**
|
|
560
|
+
* Render a capability into a host surface, with component overrides attached.
|
|
561
|
+
*
|
|
562
|
+
* The surface counterpart of {@link renderForToolbar}, and it exists for the
|
|
563
|
+
* same reason: the registry owns the component-override map, so a host calling
|
|
564
|
+
* `registration.renderSurface(...)` directly would resolve element tags against
|
|
565
|
+
* nothing and fail on every packaged capability. Overrides passed in the
|
|
566
|
+
* context still win, matching the toolbar path's precedence.
|
|
567
|
+
*/
|
|
568
|
+
renderForSurface(toolId: string, context: Omit<ToolSurfaceRenderContext, "componentOverrides"> & {
|
|
569
|
+
componentOverrides?: ToolComponentOverrides;
|
|
570
|
+
}): ToolSurfaceRenderResult | null;
|
|
361
571
|
}
|
|
@@ -55,9 +55,15 @@ function assertToolRegistrationShape(registration) {
|
|
|
55
55
|
assertNonEmptyString(registration.toolId, "toolId");
|
|
56
56
|
assertNonEmptyString(registration.name, "name");
|
|
57
57
|
assertNonEmptyString(registration.description, "description");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// A region capability renders into a host surface and has no toolbar button,
|
|
59
|
+
// so it needs neither an icon nor `renderToolbar`. Both stay required for the
|
|
60
|
+
// activations that do render a button, so no existing registration is relaxed.
|
|
61
|
+
const isRegion = registration.activation === "region";
|
|
62
|
+
if (!isRegion || registration.icon !== undefined) {
|
|
63
|
+
if (typeof registration.icon !== "string" &&
|
|
64
|
+
typeof registration.icon !== "function") {
|
|
65
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "icon" must be a string or function.`);
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
if (typeof registration.icon === "string") {
|
|
63
69
|
assertIconStringIsSafe(registration.toolId, registration.icon, "icon");
|
|
@@ -72,9 +78,30 @@ function assertToolRegistrationShape(registration) {
|
|
|
72
78
|
}
|
|
73
79
|
if (registration.activation !== undefined &&
|
|
74
80
|
registration.activation !== "toolbar-toggle" &&
|
|
75
|
-
registration.activation !== "selection-gateway"
|
|
81
|
+
registration.activation !== "selection-gateway" &&
|
|
82
|
+
registration.activation !== "region") {
|
|
76
83
|
throw new Error(`Invalid tool registration "${registration.toolId}": unsupported activation "${String(registration.activation)}".`);
|
|
77
84
|
}
|
|
85
|
+
if (registration.surfaces !== undefined &&
|
|
86
|
+
(!Array.isArray(registration.surfaces) ||
|
|
87
|
+
registration.surfaces.some((surface) => typeof surface !== "string" || surface.trim().length === 0))) {
|
|
88
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "surfaces" must be an array of non-empty strings.`);
|
|
89
|
+
}
|
|
90
|
+
if (isRegion && !registration.surfaces?.length) {
|
|
91
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": region tools must declare at least one host surface in "surfaces".`);
|
|
92
|
+
}
|
|
93
|
+
if (isRegion && typeof registration.renderSurface !== "function") {
|
|
94
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": region tools must implement "renderSurface".`);
|
|
95
|
+
}
|
|
96
|
+
if (registration.renderSurface !== undefined &&
|
|
97
|
+
typeof registration.renderSurface !== "function") {
|
|
98
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderSurface" must be a function.`);
|
|
99
|
+
}
|
|
100
|
+
if (registration.renderSurface && !registration.surfaces?.length) {
|
|
101
|
+
// A surface renderer nothing can find is a registration that silently does
|
|
102
|
+
// not render, which is the failure mode this mechanism exists to remove.
|
|
103
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderSurface" requires at least one entry in "surfaces".`);
|
|
104
|
+
}
|
|
78
105
|
if (registration.singletonScope !== undefined &&
|
|
79
106
|
registration.singletonScope !== "section") {
|
|
80
107
|
throw new Error(`Invalid tool registration "${registration.toolId}": unsupported singletonScope "${String(registration.singletonScope)}".`);
|
|
@@ -88,10 +115,33 @@ function assertToolRegistrationShape(registration) {
|
|
|
88
115
|
registration.pnpSupportIds.some((pnpId) => typeof pnpId !== "string" || pnpId.trim().length === 0))) {
|
|
89
116
|
throw new Error(`Invalid tool registration "${registration.toolId}": "pnpSupportIds" must be an array of non-empty strings.`);
|
|
90
117
|
}
|
|
91
|
-
if (
|
|
118
|
+
if (registration.activation !== "region" &&
|
|
119
|
+
typeof registration.isVisibleInContext !== "function") {
|
|
92
120
|
throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function.`);
|
|
93
121
|
}
|
|
94
|
-
if (
|
|
122
|
+
if (registration.isVisibleInContext !== undefined &&
|
|
123
|
+
typeof registration.isVisibleInContext !== "function") {
|
|
124
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function when present.`);
|
|
125
|
+
}
|
|
126
|
+
if (registration.requiresAuthoredContent !== undefined) {
|
|
127
|
+
if (typeof registration.requiresAuthoredContent !== "object" ||
|
|
128
|
+
registration.requiresAuthoredContent === null ||
|
|
129
|
+
typeof registration.requiresAuthoredContent.resolve !== "function") {
|
|
130
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "requiresAuthoredContent" must be an object with a "resolve" function.`);
|
|
131
|
+
}
|
|
132
|
+
if (!registration.pnpSupportIds?.length) {
|
|
133
|
+
// A content dependency's second job is keeping the capability out of a
|
|
134
|
+
// wholesale grant, and a host filters that by support id. Declaring one
|
|
135
|
+
// with no id to filter on would silently drop that guarantee.
|
|
136
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "requiresAuthoredContent" requires at least one entry in "pnpSupportIds", which is what a host filters a default grant list on.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (registration.renderToolbar !== undefined) {
|
|
140
|
+
if (typeof registration.renderToolbar !== "function") {
|
|
141
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderToolbar" must be a function.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (!isRegion) {
|
|
95
145
|
throw new Error(`Invalid tool registration "${registration.toolId}": "renderToolbar" must be a function.`);
|
|
96
146
|
}
|
|
97
147
|
}
|
|
@@ -251,6 +301,37 @@ export class ToolRegistry {
|
|
|
251
301
|
getToolSingletonScope(toolId) {
|
|
252
302
|
return this.get(toolId)?.singletonScope || null;
|
|
253
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Registrations that can fill a named host surface.
|
|
306
|
+
*
|
|
307
|
+
* The discovery call a renderer makes instead of naming a capability. Order
|
|
308
|
+
* follows registration order, so a host mounting several capabilities into one
|
|
309
|
+
* surface gets a stable sequence without core deciding a precedence it has no
|
|
310
|
+
* basis for.
|
|
311
|
+
*/
|
|
312
|
+
getToolsBySurface(surface) {
|
|
313
|
+
if (!surface)
|
|
314
|
+
return [];
|
|
315
|
+
return this.getAllTools().filter((tool) => typeof tool.renderSurface === "function" &&
|
|
316
|
+
tool.surfaces?.includes(surface));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Support ids belonging to capabilities that need authored content.
|
|
320
|
+
*
|
|
321
|
+
* What a host filters a default grant list on, in place of the compile-time
|
|
322
|
+
* exclusion array this replaced: granting one of these wholesale grants an
|
|
323
|
+
* accommodation to learners with no documented need for it.
|
|
324
|
+
*/
|
|
325
|
+
getContentDependentSupportIds() {
|
|
326
|
+
const ids = new Set();
|
|
327
|
+
for (const tool of this.getAllTools()) {
|
|
328
|
+
if (!tool.requiresAuthoredContent)
|
|
329
|
+
continue;
|
|
330
|
+
for (const supportId of tool.pnpSupportIds || [])
|
|
331
|
+
ids.add(supportId);
|
|
332
|
+
}
|
|
333
|
+
return [...ids].sort();
|
|
334
|
+
}
|
|
254
335
|
/**
|
|
255
336
|
* Filter tool IDs by activation type.
|
|
256
337
|
*/
|
|
@@ -279,9 +360,10 @@ export class ToolRegistry {
|
|
|
279
360
|
if (!tool.supportedLevels.includes(context.level)) {
|
|
280
361
|
continue;
|
|
281
362
|
}
|
|
282
|
-
// Pass 2: Ask tool if it's relevant
|
|
363
|
+
// Pass 2: Ask tool if it's relevant. A region capability declares no
|
|
364
|
+
// answer and has no toolbar presence, so it is never visible here.
|
|
283
365
|
try {
|
|
284
|
-
if (tool.isVisibleInContext(context)) {
|
|
366
|
+
if (tool.isVisibleInContext?.(context)) {
|
|
285
367
|
visible.push(tool);
|
|
286
368
|
}
|
|
287
369
|
}
|
|
@@ -306,6 +388,9 @@ export class ToolRegistry {
|
|
|
306
388
|
supportedLevels: tool.supportedLevels,
|
|
307
389
|
activation: tool.activation || "toolbar-toggle",
|
|
308
390
|
singletonScope: tool.singletonScope || null,
|
|
391
|
+
surfaces: tool.surfaces || [],
|
|
392
|
+
requiresAuthoredContent: Boolean(tool.requiresAuthoredContent),
|
|
393
|
+
contentDependencyDescription: tool.requiresAuthoredContent?.description ?? null,
|
|
309
394
|
}));
|
|
310
395
|
}
|
|
311
396
|
/**
|
|
@@ -402,6 +487,12 @@ export class ToolRegistry {
|
|
|
402
487
|
if (!tool) {
|
|
403
488
|
throw new Error(`Tool '${toolId}' is not registered`);
|
|
404
489
|
}
|
|
490
|
+
if (typeof tool.renderToolbar !== "function") {
|
|
491
|
+
// Naming the activation rather than "renderToolbar is not a function":
|
|
492
|
+
// the caller's mistake is asking a surface capability for a toolbar
|
|
493
|
+
// button, and it is fixed by placement config, not by the registration.
|
|
494
|
+
throw new Error(`Tool '${toolId}' has activation "${tool.activation || "toolbar-toggle"}" and renders into a host surface, not a toolbar. Remove it from toolbar placement.`);
|
|
495
|
+
}
|
|
405
496
|
const mergedContext = {
|
|
406
497
|
...toolbarContext,
|
|
407
498
|
componentOverrides: {
|
|
@@ -411,4 +502,29 @@ export class ToolRegistry {
|
|
|
411
502
|
};
|
|
412
503
|
return tool.renderToolbar(context, mergedContext);
|
|
413
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Render a capability into a host surface, with component overrides attached.
|
|
507
|
+
*
|
|
508
|
+
* The surface counterpart of {@link renderForToolbar}, and it exists for the
|
|
509
|
+
* same reason: the registry owns the component-override map, so a host calling
|
|
510
|
+
* `registration.renderSurface(...)` directly would resolve element tags against
|
|
511
|
+
* nothing and fail on every packaged capability. Overrides passed in the
|
|
512
|
+
* context still win, matching the toolbar path's precedence.
|
|
513
|
+
*/
|
|
514
|
+
renderForSurface(toolId, context) {
|
|
515
|
+
const tool = this.get(toolId);
|
|
516
|
+
if (!tool) {
|
|
517
|
+
throw new Error(`Tool '${toolId}' is not registered`);
|
|
518
|
+
}
|
|
519
|
+
if (typeof tool.renderSurface !== "function") {
|
|
520
|
+
throw new Error(`Tool '${toolId}' does not render into a host surface. Surface capabilities declare "surfaces" and implement "renderSurface".`);
|
|
521
|
+
}
|
|
522
|
+
return tool.renderSurface({
|
|
523
|
+
...context,
|
|
524
|
+
componentOverrides: {
|
|
525
|
+
...(this.componentOverrides || {}),
|
|
526
|
+
...(context.componentOverrides || {}),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
414
530
|
}
|
|
@@ -17,6 +17,7 @@ import type { AccessibilityCatalog, AssessmentEntity, AssessmentItemRef } from "
|
|
|
17
17
|
import { type CanonicalToolsConfig, type ToolPlacementConfig, type ToolPlacementLevel, type ToolPolicyConfig, type ToolProviderConfig, type ToolProvidersConfig } from "./tools-config-normalizer.js";
|
|
18
18
|
import { type ToolConfigStrictness } from "./tool-config-validation.js";
|
|
19
19
|
import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
|
|
20
|
+
import type { CatalogChangeListener } from "./AccessibilityCatalogResolver.js";
|
|
20
21
|
import { ElementToolStateStore } from "./ElementToolStateStore.js";
|
|
21
22
|
import { type FrameworkErrorModel } from "./framework-error.js";
|
|
22
23
|
import { FrameworkErrorBus, type FrameworkErrorListener } from "./framework-error-bus.js";
|
|
@@ -28,11 +29,12 @@ import type { SREMathSpeechOptions } from "./tts/math-speech.js";
|
|
|
28
29
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
29
30
|
import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
30
31
|
import type { TTSToolProviderConfig } from "./tool-providers/index.js";
|
|
31
|
-
import
|
|
32
|
-
import
|
|
32
|
+
import { ToolRegistry } from "./ToolRegistry.js";
|
|
33
|
+
import type { ResolvedToolContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap } from "./ToolRegistry.js";
|
|
34
|
+
import { type FeaturePolicyDecision, type PnpEnforcementMode, type PolicySource, type ResolvedEngineInputs, type ToolPolicyChangeListener, type ToolPolicyDecision, type ToolPolicyDecisionRequest } from "../policy/engine.js";
|
|
33
35
|
import type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionSessionPersistenceStrategy, SectionPersistenceFactoryDefaults } from "./section-controller-types.js";
|
|
34
36
|
export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionControllerLoadedRenderable, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, } from "./section-controller-types.js";
|
|
35
|
-
export type { PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest, } from "../policy/engine.js";
|
|
37
|
+
export type { FeaturePolicyDecision, PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest, } from "../policy/engine.js";
|
|
36
38
|
/**
|
|
37
39
|
* Generic tool configuration
|
|
38
40
|
*/
|
|
@@ -659,6 +661,15 @@ export declare class ToolkitCoordinator {
|
|
|
659
661
|
* Thin shim over the owned tool-policy engine.
|
|
660
662
|
*/
|
|
661
663
|
decideToolPolicy(request: ToolPolicyDecisionRequest): ToolPolicyDecision;
|
|
664
|
+
/**
|
|
665
|
+
* Resolve eligibility for one PNP/AfA feature id, independent of toolbar
|
|
666
|
+
* placement.
|
|
667
|
+
*
|
|
668
|
+
* Thin shim over the owned tool-policy engine; see
|
|
669
|
+
* {@link ToolPolicyEngine.decideFeature} for the contract, including why
|
|
670
|
+
* `pnpEnforcement` is not consulted.
|
|
671
|
+
*/
|
|
672
|
+
decideFeaturePolicy(featureId: string): FeaturePolicyDecision;
|
|
662
673
|
/**
|
|
663
674
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
664
675
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
@@ -678,6 +689,17 @@ export declare class ToolkitCoordinator {
|
|
|
678
689
|
* relying on a `disposed` event.
|
|
679
690
|
*/
|
|
680
691
|
onPolicyChange(listener: ToolPolicyChangeListener): () => void;
|
|
692
|
+
/**
|
|
693
|
+
* Subscribe to accessibility-catalog registrations and removals.
|
|
694
|
+
*
|
|
695
|
+
* Delegates to the owned resolver, the same way {@link onPolicyChange}
|
|
696
|
+
* delegates to the owned policy engine, so a consumer holding only the
|
|
697
|
+
* coordinator can react to both of the mutable inputs a catalog-backed
|
|
698
|
+
* capability depends on without reaching for the services directly.
|
|
699
|
+
*
|
|
700
|
+
* @returns Unsubscribe function
|
|
701
|
+
*/
|
|
702
|
+
onCatalogsChange(listener: CatalogChangeListener): () => void;
|
|
681
703
|
/**
|
|
682
704
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
683
705
|
*
|
|
@@ -25,7 +25,7 @@ import { TTSService } from "./TTSService.js";
|
|
|
25
25
|
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
26
26
|
import { buildRuntimeTTSConfig, resolveTTSBackend, resolveTTSRuntimeSettings, } from "./tts-runtime-config.js";
|
|
27
27
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
28
|
-
import {
|
|
28
|
+
import { ToolRegistry } from "./ToolRegistry.js";
|
|
29
29
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
30
30
|
import { resolveDefaultPnpEnforcement } from "../policy/internal.js";
|
|
31
31
|
const isPlainRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -190,7 +190,13 @@ export class ToolkitCoordinator {
|
|
|
190
190
|
boundCurrentItemRef = null;
|
|
191
191
|
static resolveConfig(config) {
|
|
192
192
|
const strictness = normalizeToolConfigStrictness(config.toolConfigStrictness);
|
|
193
|
-
|
|
193
|
+
// An empty registry when the host supplies none. This package no longer
|
|
194
|
+
// holds the packaged capability set, so there is nothing to fall back to —
|
|
195
|
+
// a host that wants stock tools passes a registry from
|
|
196
|
+
// `@pie-players/pie-default-tool-loaders`. Tool-id validation is skipped
|
|
197
|
+
// against an empty registry (with a diagnostic saying so) rather than
|
|
198
|
+
// rejecting every configured id.
|
|
199
|
+
const toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
194
200
|
const normalized = config.deferToolConfigValidation === true
|
|
195
201
|
? normalizeToolsConfig(config.tools)
|
|
196
202
|
: normalizeAndValidateToolsConfig(config.tools, {
|
|
@@ -232,8 +238,7 @@ export class ToolkitCoordinator {
|
|
|
232
238
|
const resolvedConfig = ToolkitCoordinator.resolveConfig(config);
|
|
233
239
|
this.assessmentId = resolvedConfig.assessmentId;
|
|
234
240
|
this.config = resolvedConfig;
|
|
235
|
-
this.toolRegistry =
|
|
236
|
-
resolvedConfig.toolRegistry ?? createPackagedToolRegistry();
|
|
241
|
+
this.toolRegistry = resolvedConfig.toolRegistry ?? new ToolRegistry();
|
|
237
242
|
this.installToolContextResolvers(resolvedConfig.toolContextResolvers);
|
|
238
243
|
this.hooks = resolvedConfig.hooks ?? {};
|
|
239
244
|
this.lazyInit = config.lazyInit === true;
|
|
@@ -1268,13 +1273,16 @@ export class ToolkitCoordinator {
|
|
|
1268
1273
|
if (canUseEventTarget) {
|
|
1269
1274
|
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
1270
1275
|
}
|
|
1271
|
-
else if (assignedHandler &&
|
|
1276
|
+
else if (assignedHandler &&
|
|
1277
|
+
synth.onvoiceschanged === onVoicesChanged) {
|
|
1272
1278
|
synth.onvoiceschanged = previousHandler;
|
|
1273
1279
|
}
|
|
1274
1280
|
resolve();
|
|
1275
1281
|
};
|
|
1276
1282
|
const onVoicesChanged = (event) => {
|
|
1277
|
-
if (!canUseEventTarget &&
|
|
1283
|
+
if (!canUseEventTarget &&
|
|
1284
|
+
typeof previousHandler === "function" &&
|
|
1285
|
+
event) {
|
|
1278
1286
|
previousHandler.call(synth, event);
|
|
1279
1287
|
}
|
|
1280
1288
|
finish();
|
|
@@ -1371,6 +1379,15 @@ export class ToolkitCoordinator {
|
|
|
1371
1379
|
if (toolId === "tts") {
|
|
1372
1380
|
throw new Error(`Tool id "tts" is no longer supported. Use "textToSpeech".`);
|
|
1373
1381
|
}
|
|
1382
|
+
// An empty registry means the host supplied none, not that every id is
|
|
1383
|
+
// wrong. There is nothing to check an id against, and throwing turns a
|
|
1384
|
+
// host's every tool-config call into an exception — including the calls
|
|
1385
|
+
// this coordinator's own default-provider block provokes, which is how a
|
|
1386
|
+
// host that passes no registry ended up unable to read its own config.
|
|
1387
|
+
// `normalizeAndValidateToolsConfig` already reports the missing registry
|
|
1388
|
+
// once, as `tools.registryUnavailable`; a second report per call is noise.
|
|
1389
|
+
if (this.toolRegistry.getAllToolIds().length === 0)
|
|
1390
|
+
return;
|
|
1374
1391
|
if (!this.toolRegistry.get(toolId)) {
|
|
1375
1392
|
throw new Error(`Unknown tool id "${toolId}".`);
|
|
1376
1393
|
}
|
|
@@ -1518,6 +1535,17 @@ export class ToolkitCoordinator {
|
|
|
1518
1535
|
decideToolPolicy(request) {
|
|
1519
1536
|
return this.policyEngine.decide(request);
|
|
1520
1537
|
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Resolve eligibility for one PNP/AfA feature id, independent of toolbar
|
|
1540
|
+
* placement.
|
|
1541
|
+
*
|
|
1542
|
+
* Thin shim over the owned tool-policy engine; see
|
|
1543
|
+
* {@link ToolPolicyEngine.decideFeature} for the contract, including why
|
|
1544
|
+
* `pnpEnforcement` is not consulted.
|
|
1545
|
+
*/
|
|
1546
|
+
decideFeaturePolicy(featureId) {
|
|
1547
|
+
return this.policyEngine.decideFeature(featureId);
|
|
1548
|
+
}
|
|
1521
1549
|
/**
|
|
1522
1550
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
1523
1551
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
@@ -1539,6 +1567,19 @@ export class ToolkitCoordinator {
|
|
|
1539
1567
|
onPolicyChange(listener) {
|
|
1540
1568
|
return this.policyEngine.onPolicyChange(listener);
|
|
1541
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Subscribe to accessibility-catalog registrations and removals.
|
|
1572
|
+
*
|
|
1573
|
+
* Delegates to the owned resolver, the same way {@link onPolicyChange}
|
|
1574
|
+
* delegates to the owned policy engine, so a consumer holding only the
|
|
1575
|
+
* coordinator can react to both of the mutable inputs a catalog-backed
|
|
1576
|
+
* capability depends on without reaching for the services directly.
|
|
1577
|
+
*
|
|
1578
|
+
* @returns Unsubscribe function
|
|
1579
|
+
*/
|
|
1580
|
+
onCatalogsChange(listener) {
|
|
1581
|
+
return this.catalogResolver.onCatalogsChange(listener);
|
|
1582
|
+
}
|
|
1542
1583
|
/**
|
|
1543
1584
|
* Bind (or clear) the active assessment for PNP/profile policy decisions.
|
|
1544
1585
|
*
|