@pie-players/pie-assessment-toolkit 0.3.64 → 0.3.66
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 +81 -33
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +9 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +51 -0
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +10 -5
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/PolicySource.js +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +7 -3
- package/dist/policy/core/ToolPolicyEngine.js +11 -3
- package/dist/policy/core/compose-decision.d.ts +3 -2
- package/dist/policy/core/compose-decision.js +3 -2
- package/dist/policy/core/decision-types.d.ts +2 -2
- package/dist/policy/core/decision-types.js +2 -2
- package/dist/policy/core/feature-decision.d.ts +28 -1
- package/dist/policy/core/feature-decision.js +25 -3
- package/dist/policy/core/pnp-policy-inputs.d.ts +2 -2
- package/dist/policy/core/pnp-policy-inputs.js +2 -2
- package/dist/policy/core/provenance.d.ts +4 -1
- package/dist/policy/core/provenance.js +4 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +2 -1
- package/dist/policy/sources/PnpPolicySource.js +2 -1
- 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/runtime/core/engine-transition.js +3 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
- package/dist/services/AccessibilityCatalogResolver.js +180 -11
- package/dist/services/TTSService.d.ts +11 -0
- package/dist/services/TTSService.js +220 -32
- package/dist/services/ToolRegistry.d.ts +259 -8
- package/dist/services/ToolRegistry.js +181 -11
- package/dist/services/ToolkitCoordinator.d.ts +20 -2
- package/dist/services/ToolkitCoordinator.js +47 -8
- package/dist/services/catalog-owner.d.ts +71 -0
- package/dist/services/catalog-owner.js +64 -0
- package/dist/services/createDefaultToolRegistry.d.ts +25 -58
- package/dist/services/createDefaultToolRegistry.js +24 -104
- package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -14
- package/dist/services/defaultPersonalNeedsProfile.js +17 -38
- package/dist/services/framework-error.d.ts +1 -1
- package/dist/services/interfaces.d.ts +15 -3
- package/dist/services/pnp-standard-features.d.ts +1 -1
- 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 +191 -24
- package/dist/services/tts-runtime-config.js +7 -2
- package/dist/tools/internal.d.ts +35 -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-3cppre9r.js +0 -51
- package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
- package/dist/runtime/catalog-registration.d.ts +0 -67
- package/dist/runtime/catalog-registration.js +0 -86
- package/dist/services/sign-language-cards.d.ts +0 -82
- package/dist/services/sign-language-cards.js +0 -133
- 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,17 +4,32 @@
|
|
|
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 { CatalogOwnerSnapshot } from "./AccessibilityCatalogResolver.js";
|
|
7
8
|
import type { ToolContext, ToolLevel } from "./tool-context.js";
|
|
8
9
|
import type { ToolComponentOverrides } from "../tools/tool-tag-map.js";
|
|
9
|
-
import type { ElementToolStateStoreApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi } from "./interfaces.js";
|
|
10
|
+
import type { AccessibilityCatalogResolverApi, ElementToolStateStoreApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi } from "./interfaces.js";
|
|
10
11
|
import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
11
12
|
import type { ToolProviderConfig as ToolRuntimeConfig } from "./tools-config-normalizer.js";
|
|
12
13
|
import type { ToolConfigDiagnostic } from "./tool-config-validation.js";
|
|
13
14
|
export type ToolModuleLoader = () => Promise<unknown>;
|
|
15
|
+
export type ToolRegistryChangeKind = "register" | "override" | "unregister" | "clear" | "component-overrides" | "module-loaders";
|
|
16
|
+
/** A successful mutation to discoverable or rendering registry state. */
|
|
17
|
+
export interface ToolRegistryChangeEvent {
|
|
18
|
+
kind: ToolRegistryChangeKind;
|
|
19
|
+
toolIds: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
export type ToolRegistryChangeListener = (event: ToolRegistryChangeEvent) => void;
|
|
14
22
|
export interface ToolToolbarButtonDefinition {
|
|
15
23
|
toolId: string;
|
|
16
24
|
label: string;
|
|
17
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Optional to match what the renderers already do: `ToolButton.svelte` and
|
|
27
|
+
* `ItemToolBar.svelte` both guard on `button.icon`, and `ToolbarItem.icon` is
|
|
28
|
+
* already optional, so requiring it here claimed a guarantee nothing relied
|
|
29
|
+
* on. A registration that renders a button still has to declare an icon —
|
|
30
|
+
* `assertToolRegistrationShape` enforces that.
|
|
31
|
+
*/
|
|
32
|
+
icon?: string;
|
|
18
33
|
ariaLabel: string;
|
|
19
34
|
tooltip?: string;
|
|
20
35
|
onClick: () => void;
|
|
@@ -148,8 +163,147 @@ export interface ToolToolbarRenderResult {
|
|
|
148
163
|
sync?: () => void;
|
|
149
164
|
subscribeActive?: (callback: (active: boolean) => void) => () => void;
|
|
150
165
|
}
|
|
151
|
-
export type ToolActivation = "toolbar-toggle" | "selection-gateway";
|
|
166
|
+
export type ToolActivation = "toolbar-toggle" | "selection-gateway" | "region";
|
|
152
167
|
export type ToolSingletonScope = "section";
|
|
168
|
+
/**
|
|
169
|
+
* Services a host hands a capability rendering into one of its surfaces.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately the same three references a toolbar tool reaches through
|
|
172
|
+
* `ToolbarContext`, and no more: a capability that needs the coordinator can ask
|
|
173
|
+
* it for anything else. Passing the host's own component or state would make the
|
|
174
|
+
* registration depend on which renderer mounted it.
|
|
175
|
+
*/
|
|
176
|
+
export interface ToolSurfaceServices {
|
|
177
|
+
toolkitCoordinator: ToolkitCoordinatorApi | null;
|
|
178
|
+
ttsService: TtsServiceApi | null;
|
|
179
|
+
catalogResolver: AccessibilityCatalogResolverApi | null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* What a host tells a capability when asking it to fill a surface.
|
|
183
|
+
*
|
|
184
|
+
* `surface` is a host-defined slot name. Core defines none and validates only
|
|
185
|
+
* that a region capability claims at least one, so a host can open a new surface
|
|
186
|
+
* without a change here and a capability can declare which of a host's surfaces
|
|
187
|
+
* it fits. Section-player ships `"content-lead"` and `"content-media"` on item
|
|
188
|
+
* and passage cards, plus `"section-overlay"` at section scope.
|
|
189
|
+
*
|
|
190
|
+
* `content` carries whatever the capability's own `requiresAuthoredContent`
|
|
191
|
+
* resolved, so the host neither inspects nor names it — it hands back what the
|
|
192
|
+
* capability asked for.
|
|
193
|
+
*/
|
|
194
|
+
export interface ToolSurfaceRenderContext {
|
|
195
|
+
toolId: string;
|
|
196
|
+
/** The PNP/AfA support id policy granted for this render. */
|
|
197
|
+
featureId: string;
|
|
198
|
+
/** Host slot being filled. */
|
|
199
|
+
surface: string;
|
|
200
|
+
/** Feature parameters from the policy decision, if any. */
|
|
201
|
+
parameters?: unknown;
|
|
202
|
+
/** Resolved content dependency, when the capability declares one. */
|
|
203
|
+
content?: unknown;
|
|
204
|
+
services: ToolSurfaceServices;
|
|
205
|
+
componentOverrides?: ToolComponentOverrides;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* What a host tells a capability when asking whether the content it needs is
|
|
209
|
+
* present.
|
|
210
|
+
*/
|
|
211
|
+
export interface ToolContentDependencyContext {
|
|
212
|
+
/** The PNP/AfA support id being resolved. */
|
|
213
|
+
featureId: string;
|
|
214
|
+
/** Feature parameters from the policy decision, if any. */
|
|
215
|
+
parameters?: unknown;
|
|
216
|
+
/**
|
|
217
|
+
* Owner-scoped catalog cards, or `null` when no resolver is available.
|
|
218
|
+
*
|
|
219
|
+
* The catalog module has already applied item/passage/model traversal and
|
|
220
|
+
* registration precedence. A capability interprets its own card type without
|
|
221
|
+
* reconstructing the owner scope or reading the raw entity.
|
|
222
|
+
*/
|
|
223
|
+
catalogs: CatalogOwnerSnapshot | null;
|
|
224
|
+
/**
|
|
225
|
+
* Whether policy granted one of this capability's support ids.
|
|
226
|
+
*
|
|
227
|
+
* `false` reaches `resolve` only for a capability that declares
|
|
228
|
+
* {@link ToolRegistration.resolvesWithoutGrant}, and it is the signal that the
|
|
229
|
+
* capability must answer from the content alone. Everything else sees `true`,
|
|
230
|
+
* because the host asks about eligibility first and stops there.
|
|
231
|
+
*/
|
|
232
|
+
granted: boolean;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A capability's declaration that it needs authored content to have anything to
|
|
236
|
+
* show, and the check that decides whether that content is present.
|
|
237
|
+
*
|
|
238
|
+
* This is the resource half of AfA's PNP/DRD pair. Signing needs an authored
|
|
239
|
+
* catalog card, braille a transcription, authored SSML a `<speak>` in that item.
|
|
240
|
+
* It is intrinsic to the capability, unlike eligibility tier, which is a property
|
|
241
|
+
* of the program.
|
|
242
|
+
*
|
|
243
|
+
* Two independent things follow from declaring it, and both used to be done by
|
|
244
|
+
* naming ids in core:
|
|
245
|
+
*
|
|
246
|
+
* 1. **Availability is grant AND content.** The host renders only when policy
|
|
247
|
+
* granted the feature *and* `resolve` returned something. Neither half
|
|
248
|
+
* implies the other and neither is a default, so a learner with the
|
|
249
|
+
* accommodation still sees nothing on an item that carries no resource — no
|
|
250
|
+
* dead affordance.
|
|
251
|
+
* 2. **It is not granted wholesale.** A host building a default grant list
|
|
252
|
+
* filters on this declaration instead of on a compile-time array of ids it
|
|
253
|
+
* cannot extend. `@pie-players/pie-default-tool-loaders` asserts its
|
|
254
|
+
* universal preset holds no id belonging to a capability that declares one.
|
|
255
|
+
*
|
|
256
|
+
* `resolve` returns the resolved content, which the host hands straight back
|
|
257
|
+
* through `ToolSurfaceRenderContext.content` without inspecting it. That is what
|
|
258
|
+
* keeps the resolver and the host from knowing which accommodation they are
|
|
259
|
+
* resolving.
|
|
260
|
+
*
|
|
261
|
+
* Resolvable only on a surface the host renders per item or per passage. The
|
|
262
|
+
* owner snapshot represents an item/model or passage, never a section, because a DRD
|
|
263
|
+
* resource pairs with a piece of content and not with a container. A capability
|
|
264
|
+
* declaring one and claiming a section-scoped surface is declined there rather
|
|
265
|
+
* than mounted with no content. `resolve` is also synchronous — a capability whose
|
|
266
|
+
* resource has to be fetched resolves the reference here and fetches inside its
|
|
267
|
+
* own element, where it can show its own pending state.
|
|
268
|
+
*/
|
|
269
|
+
export interface ToolContentDependency {
|
|
270
|
+
/**
|
|
271
|
+
* The resolved content, or `null` when the item carries none.
|
|
272
|
+
*
|
|
273
|
+
* Must be JSON-serializable. A host re-resolves on every policy and catalog
|
|
274
|
+
* signal and compares the answer structurally to decide whether anything moved,
|
|
275
|
+
* because every resolution builds fresh objects and identity would report a
|
|
276
|
+
* change each time. A `Map`, function, DOM node, cyclic value, or other
|
|
277
|
+
* non-serializable result is rejected for that capability and reported as a
|
|
278
|
+
* recoverable surface warning instead of escaping into the player.
|
|
279
|
+
*/
|
|
280
|
+
resolve(context: ToolContentDependencyContext): unknown | null;
|
|
281
|
+
/**
|
|
282
|
+
* Optional human-readable description of what has to be authored, for a
|
|
283
|
+
* policy debugger explaining why an otherwise-granted capability is absent.
|
|
284
|
+
*/
|
|
285
|
+
description?: string;
|
|
286
|
+
}
|
|
287
|
+
export interface ToolSurfaceRenderResult {
|
|
288
|
+
/** Element for the host to mount into its surface. */
|
|
289
|
+
element: HTMLElement;
|
|
290
|
+
/** Accessible name for the surface, when the capability owns that wording. */
|
|
291
|
+
ariaLabel?: string;
|
|
292
|
+
/**
|
|
293
|
+
* Reapply props after policy, parameters or content change.
|
|
294
|
+
*
|
|
295
|
+
* Takes the current context rather than closing over the one captured at
|
|
296
|
+
* render: the whole point of reconciling by `toolId` instead of remounting is
|
|
297
|
+
* that a re-resolve reaches the mounted element, and a closure over the
|
|
298
|
+
* render-time context re-applies the values the host already had. A signed
|
|
299
|
+
* alternate re-resolved to a different recording — a live `signLang` change, or
|
|
300
|
+
* a catalog registering after first paint — would otherwise leave the learner
|
|
301
|
+
* watching the previous one with no error anywhere.
|
|
302
|
+
*/
|
|
303
|
+
sync?: (context: ToolSurfaceRenderContext) => void;
|
|
304
|
+
/** Release listeners and media before the host unmounts the element. */
|
|
305
|
+
destroy?: () => void;
|
|
306
|
+
}
|
|
153
307
|
/**
|
|
154
308
|
* Tool registration interface
|
|
155
309
|
*/
|
|
@@ -160,16 +314,31 @@ export interface ToolRegistration {
|
|
|
160
314
|
name: string;
|
|
161
315
|
/** Description of what the tool does */
|
|
162
316
|
description: string;
|
|
163
|
-
/**
|
|
164
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Icon identifier or SVG string. Required for the activations that render a
|
|
319
|
+
* toolbar button; a region capability has no button, so it has no icon.
|
|
320
|
+
*/
|
|
321
|
+
icon?: string | ((context: ToolContext) => string);
|
|
165
322
|
/** Which levels this tool supports */
|
|
166
323
|
supportedLevels: ToolLevel[];
|
|
167
324
|
/**
|
|
168
325
|
* Activation model for this tool.
|
|
169
326
|
* - toolbar-toggle: rendered as a toolbar button (default)
|
|
170
327
|
* - selection-gateway: rendered as a singleton selection-driven gateway
|
|
328
|
+
* - region: rendered into a host surface, with no toolbar button
|
|
171
329
|
*/
|
|
172
330
|
activation?: ToolActivation;
|
|
331
|
+
/**
|
|
332
|
+
* Host surfaces this capability can fill. Required for `activation: "region"`
|
|
333
|
+
* and meaningful for any activation whose capability also has a non-toolbar
|
|
334
|
+
* surface — the annotation toolbar is both a toolbar button and a
|
|
335
|
+
* section-scoped singleton.
|
|
336
|
+
*
|
|
337
|
+
* Names are the host's, not core's. A host discovers what it can mount by
|
|
338
|
+
* asking {@link ToolRegistry.getToolsBySurface}, which is what keeps a
|
|
339
|
+
* renderer from naming a capability.
|
|
340
|
+
*/
|
|
341
|
+
surfaces?: string[];
|
|
173
342
|
/**
|
|
174
343
|
* Optional singleton scope for activation models that mount exactly one instance.
|
|
175
344
|
*/
|
|
@@ -180,6 +349,30 @@ export interface ToolRegistration {
|
|
|
180
349
|
* Example: ['calculator', 'basic-calculator', 'scientific-calculator']
|
|
181
350
|
*/
|
|
182
351
|
pnpSupportIds?: string[];
|
|
352
|
+
/**
|
|
353
|
+
* Authored content this capability needs before it has anything to show.
|
|
354
|
+
*
|
|
355
|
+
* Declaring it makes availability "grant AND content", and excludes the
|
|
356
|
+
* capability from any wholesale default grant. See
|
|
357
|
+
* {@link ToolContentDependency}.
|
|
358
|
+
*/
|
|
359
|
+
requiresAuthoredContent?: ToolContentDependency;
|
|
360
|
+
/**
|
|
361
|
+
* Ask this capability for its content even when policy granted nothing.
|
|
362
|
+
*
|
|
363
|
+
* Only meaningful together with {@link ToolRegistration.requiresAuthoredContent},
|
|
364
|
+
* and only correct for a capability whose authored content can declare itself
|
|
365
|
+
* *presentation* rather than an accommodation — content authored to be delivered
|
|
366
|
+
* that way to everyone, which no profile grants and none revokes. Such a
|
|
367
|
+
* capability must return `null` from `resolve` when its content is the
|
|
368
|
+
* accommodation kind and the grant is absent, and the `granted` flag on the
|
|
369
|
+
* context is how it tells the two apart.
|
|
370
|
+
*
|
|
371
|
+
* Without this, "no grant" ends the question before content is consulted, which
|
|
372
|
+
* is the right default: it is what keeps an accommodation off the item of a
|
|
373
|
+
* learner with no documented need.
|
|
374
|
+
*/
|
|
375
|
+
resolvesWithoutGrant?: boolean;
|
|
183
376
|
/**
|
|
184
377
|
* Optional provider registration metadata.
|
|
185
378
|
* When present, ToolkitCoordinator can register provider(s) generically
|
|
@@ -196,12 +389,31 @@ export interface ToolRegistration {
|
|
|
196
389
|
* Pass 2: Tool decides if it's relevant in this context
|
|
197
390
|
* Called ONLY if orchestrator has already allowed the tool (Pass 1)
|
|
198
391
|
*
|
|
392
|
+
* Required for the toolbar activations, and meaningless for `activation:
|
|
393
|
+
* "region"`: a region capability has no toolbar presence to be relevant to, and
|
|
394
|
+
* the question it *would* answer — is there anything to show here — is
|
|
395
|
+
* `requiresAuthoredContent`. A registration that omits this is never returned
|
|
396
|
+
* by `getVisibleTools`.
|
|
397
|
+
*
|
|
199
398
|
* @param context - Rich context about where tool is being evaluated
|
|
200
399
|
* @returns true if tool should be visible, false to hide
|
|
201
400
|
*/
|
|
202
|
-
isVisibleInContext(context: ToolContext): boolean;
|
|
203
|
-
/**
|
|
204
|
-
|
|
401
|
+
isVisibleInContext?(context: ToolContext): boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Toolbar render contract. Required for `toolbar-toggle` and
|
|
404
|
+
* `selection-gateway`; a region capability renders through
|
|
405
|
+
* {@link ToolRegistration.renderSurface} instead.
|
|
406
|
+
*/
|
|
407
|
+
renderToolbar?(context: ToolContext, toolbarContext: ToolbarContext): ToolToolbarRenderResult | null;
|
|
408
|
+
/**
|
|
409
|
+
* Render into one of the host surfaces this capability declares.
|
|
410
|
+
*
|
|
411
|
+
* Returning `null` means "nothing to show for this render" and is not an
|
|
412
|
+
* error — a capability may decline once the host has already granted and
|
|
413
|
+
* resolved content. The host mounts the returned element and calls `sync()`
|
|
414
|
+
* when policy, parameters or content move.
|
|
415
|
+
*/
|
|
416
|
+
renderSurface?(context: ToolSurfaceRenderContext): ToolSurfaceRenderResult | null;
|
|
205
417
|
}
|
|
206
418
|
/**
|
|
207
419
|
* Tool Registry
|
|
@@ -215,6 +427,13 @@ export declare class ToolRegistry {
|
|
|
215
427
|
private moduleLoaders;
|
|
216
428
|
private loadedToolModules;
|
|
217
429
|
private moduleLoadPromises;
|
|
430
|
+
private changeListeners;
|
|
431
|
+
private emitChange;
|
|
432
|
+
/**
|
|
433
|
+
* Observe successful registry mutations. Delivery is synchronous, listener
|
|
434
|
+
* failures are isolated, and the returned unsubscribe is idempotent.
|
|
435
|
+
*/
|
|
436
|
+
onRegistryChange(listener: ToolRegistryChangeListener): () => void;
|
|
218
437
|
/**
|
|
219
438
|
* Normalize a single tool alias to canonical toolId.
|
|
220
439
|
*/
|
|
@@ -290,6 +509,23 @@ export declare class ToolRegistry {
|
|
|
290
509
|
* Resolve singleton scope for a tool when present.
|
|
291
510
|
*/
|
|
292
511
|
getToolSingletonScope(toolId: string): ToolSingletonScope | null;
|
|
512
|
+
/**
|
|
513
|
+
* Registrations that can fill a named host surface.
|
|
514
|
+
*
|
|
515
|
+
* The discovery call a renderer makes instead of naming a capability. Order
|
|
516
|
+
* follows registration order, so a host mounting several capabilities into one
|
|
517
|
+
* surface gets a stable sequence without core deciding a precedence it has no
|
|
518
|
+
* basis for.
|
|
519
|
+
*/
|
|
520
|
+
getToolsBySurface(surface: string): ToolRegistration[];
|
|
521
|
+
/**
|
|
522
|
+
* Support ids belonging to capabilities that need authored content.
|
|
523
|
+
*
|
|
524
|
+
* What a host filters a default grant list on, in place of the compile-time
|
|
525
|
+
* exclusion array this replaced: granting one of these wholesale grants an
|
|
526
|
+
* accommodation to learners with no documented need for it.
|
|
527
|
+
*/
|
|
528
|
+
getContentDependentSupportIds(): string[];
|
|
293
529
|
/**
|
|
294
530
|
* Filter tool IDs by activation type.
|
|
295
531
|
*/
|
|
@@ -319,6 +555,9 @@ export declare class ToolRegistry {
|
|
|
319
555
|
supportedLevels: ToolLevel[];
|
|
320
556
|
activation: ToolActivation;
|
|
321
557
|
singletonScope: ToolSingletonScope | null;
|
|
558
|
+
surfaces: string[];
|
|
559
|
+
requiresAuthoredContent: boolean;
|
|
560
|
+
contentDependencyDescription: string | null;
|
|
322
561
|
}>;
|
|
323
562
|
/**
|
|
324
563
|
* Generate PNP support IDs from enabled tools
|
|
@@ -358,4 +597,16 @@ export declare class ToolRegistry {
|
|
|
358
597
|
* Render a tool for toolbar use with component overrides attached.
|
|
359
598
|
*/
|
|
360
599
|
renderForToolbar(toolId: string, context: ToolContext, toolbarContext: ToolbarContext): ToolToolbarRenderResult | null;
|
|
600
|
+
/**
|
|
601
|
+
* Render a capability into a host surface, with component overrides attached.
|
|
602
|
+
*
|
|
603
|
+
* The surface counterpart of {@link renderForToolbar}, and it exists for the
|
|
604
|
+
* same reason: the registry owns the component-override map, so a host calling
|
|
605
|
+
* `registration.renderSurface(...)` directly would resolve element tags against
|
|
606
|
+
* nothing and fail on every packaged capability. Overrides passed in the
|
|
607
|
+
* context still win, matching the toolbar path's precedence.
|
|
608
|
+
*/
|
|
609
|
+
renderForSurface(toolId: string, context: Omit<ToolSurfaceRenderContext, "componentOverrides"> & {
|
|
610
|
+
componentOverrides?: ToolComponentOverrides;
|
|
611
|
+
}): ToolSurfaceRenderResult | null;
|
|
361
612
|
}
|
|
@@ -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,41 @@ 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.resolvesWithoutGrant !== undefined &&
|
|
97
|
+
typeof registration.resolvesWithoutGrant !== "boolean") {
|
|
98
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "resolvesWithoutGrant" must be a boolean.`);
|
|
99
|
+
}
|
|
100
|
+
if (registration.resolvesWithoutGrant &&
|
|
101
|
+
!registration.requiresAuthoredContent) {
|
|
102
|
+
// The flag only decides whether content is consulted without a grant, so on a
|
|
103
|
+
// capability with no content dependency it reads as "granted to everyone" and
|
|
104
|
+
// does nothing at all.
|
|
105
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "resolvesWithoutGrant" requires "requiresAuthoredContent".`);
|
|
106
|
+
}
|
|
107
|
+
if (registration.renderSurface !== undefined &&
|
|
108
|
+
typeof registration.renderSurface !== "function") {
|
|
109
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderSurface" must be a function.`);
|
|
110
|
+
}
|
|
111
|
+
if (registration.renderSurface && !registration.surfaces?.length) {
|
|
112
|
+
// A surface renderer nothing can find is a registration that silently does
|
|
113
|
+
// not render, which is the failure mode this mechanism exists to remove.
|
|
114
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderSurface" requires at least one entry in "surfaces".`);
|
|
115
|
+
}
|
|
78
116
|
if (registration.singletonScope !== undefined &&
|
|
79
117
|
registration.singletonScope !== "section") {
|
|
80
118
|
throw new Error(`Invalid tool registration "${registration.toolId}": unsupported singletonScope "${String(registration.singletonScope)}".`);
|
|
@@ -88,10 +126,33 @@ function assertToolRegistrationShape(registration) {
|
|
|
88
126
|
registration.pnpSupportIds.some((pnpId) => typeof pnpId !== "string" || pnpId.trim().length === 0))) {
|
|
89
127
|
throw new Error(`Invalid tool registration "${registration.toolId}": "pnpSupportIds" must be an array of non-empty strings.`);
|
|
90
128
|
}
|
|
91
|
-
if (
|
|
129
|
+
if (registration.activation !== "region" &&
|
|
130
|
+
typeof registration.isVisibleInContext !== "function") {
|
|
92
131
|
throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function.`);
|
|
93
132
|
}
|
|
94
|
-
if (
|
|
133
|
+
if (registration.isVisibleInContext !== undefined &&
|
|
134
|
+
typeof registration.isVisibleInContext !== "function") {
|
|
135
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "isVisibleInContext" must be a function when present.`);
|
|
136
|
+
}
|
|
137
|
+
if (registration.requiresAuthoredContent !== undefined) {
|
|
138
|
+
if (typeof registration.requiresAuthoredContent !== "object" ||
|
|
139
|
+
registration.requiresAuthoredContent === null ||
|
|
140
|
+
typeof registration.requiresAuthoredContent.resolve !== "function") {
|
|
141
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "requiresAuthoredContent" must be an object with a "resolve" function.`);
|
|
142
|
+
}
|
|
143
|
+
if (!registration.pnpSupportIds?.length) {
|
|
144
|
+
// A content dependency's second job is keeping the capability out of a
|
|
145
|
+
// wholesale grant, and a host filters that by support id. Declaring one
|
|
146
|
+
// with no id to filter on would silently drop that guarantee.
|
|
147
|
+
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.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (registration.renderToolbar !== undefined) {
|
|
151
|
+
if (typeof registration.renderToolbar !== "function") {
|
|
152
|
+
throw new Error(`Invalid tool registration "${registration.toolId}": "renderToolbar" must be a function.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (!isRegion) {
|
|
95
156
|
throw new Error(`Invalid tool registration "${registration.toolId}": "renderToolbar" must be a function.`);
|
|
96
157
|
}
|
|
97
158
|
}
|
|
@@ -107,6 +168,31 @@ export class ToolRegistry {
|
|
|
107
168
|
moduleLoaders = new Map();
|
|
108
169
|
loadedToolModules = new Set();
|
|
109
170
|
moduleLoadPromises = new Map();
|
|
171
|
+
changeListeners = new Set();
|
|
172
|
+
emitChange(event) {
|
|
173
|
+
for (const listener of this.changeListeners) {
|
|
174
|
+
try {
|
|
175
|
+
listener(event);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
console.warn("[ToolRegistry] change listener failed:", error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Observe successful registry mutations. Delivery is synchronous, listener
|
|
184
|
+
* failures are isolated, and the returned unsubscribe is idempotent.
|
|
185
|
+
*/
|
|
186
|
+
onRegistryChange(listener) {
|
|
187
|
+
this.changeListeners.add(listener);
|
|
188
|
+
let subscribed = true;
|
|
189
|
+
return () => {
|
|
190
|
+
if (!subscribed)
|
|
191
|
+
return;
|
|
192
|
+
subscribed = false;
|
|
193
|
+
this.changeListeners.delete(listener);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
110
196
|
/**
|
|
111
197
|
* Normalize a single tool alias to canonical toolId.
|
|
112
198
|
*/
|
|
@@ -140,6 +226,7 @@ export class ToolRegistry {
|
|
|
140
226
|
this.pnpIndex.get(pnpId).add(registration.toolId);
|
|
141
227
|
}
|
|
142
228
|
}
|
|
229
|
+
this.emitChange({ kind: "register", toolIds: [registration.toolId] });
|
|
143
230
|
}
|
|
144
231
|
/**
|
|
145
232
|
* Override an existing tool registration
|
|
@@ -169,6 +256,7 @@ export class ToolRegistry {
|
|
|
169
256
|
this.pnpIndex.get(pnpId).add(registration.toolId);
|
|
170
257
|
}
|
|
171
258
|
}
|
|
259
|
+
this.emitChange({ kind: "override", toolIds: [registration.toolId] });
|
|
172
260
|
}
|
|
173
261
|
/**
|
|
174
262
|
* Unregister a tool
|
|
@@ -186,6 +274,7 @@ export class ToolRegistry {
|
|
|
186
274
|
}
|
|
187
275
|
}
|
|
188
276
|
this.tools.delete(toolId);
|
|
277
|
+
this.emitChange({ kind: "unregister", toolIds: [toolId] });
|
|
189
278
|
}
|
|
190
279
|
/**
|
|
191
280
|
* Get a tool registration by ID
|
|
@@ -251,6 +340,37 @@ export class ToolRegistry {
|
|
|
251
340
|
getToolSingletonScope(toolId) {
|
|
252
341
|
return this.get(toolId)?.singletonScope || null;
|
|
253
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Registrations that can fill a named host surface.
|
|
345
|
+
*
|
|
346
|
+
* The discovery call a renderer makes instead of naming a capability. Order
|
|
347
|
+
* follows registration order, so a host mounting several capabilities into one
|
|
348
|
+
* surface gets a stable sequence without core deciding a precedence it has no
|
|
349
|
+
* basis for.
|
|
350
|
+
*/
|
|
351
|
+
getToolsBySurface(surface) {
|
|
352
|
+
if (!surface)
|
|
353
|
+
return [];
|
|
354
|
+
return this.getAllTools().filter((tool) => typeof tool.renderSurface === "function" &&
|
|
355
|
+
tool.surfaces?.includes(surface));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Support ids belonging to capabilities that need authored content.
|
|
359
|
+
*
|
|
360
|
+
* What a host filters a default grant list on, in place of the compile-time
|
|
361
|
+
* exclusion array this replaced: granting one of these wholesale grants an
|
|
362
|
+
* accommodation to learners with no documented need for it.
|
|
363
|
+
*/
|
|
364
|
+
getContentDependentSupportIds() {
|
|
365
|
+
const ids = new Set();
|
|
366
|
+
for (const tool of this.getAllTools()) {
|
|
367
|
+
if (!tool.requiresAuthoredContent)
|
|
368
|
+
continue;
|
|
369
|
+
for (const supportId of tool.pnpSupportIds || [])
|
|
370
|
+
ids.add(supportId);
|
|
371
|
+
}
|
|
372
|
+
return [...ids].sort();
|
|
373
|
+
}
|
|
254
374
|
/**
|
|
255
375
|
* Filter tool IDs by activation type.
|
|
256
376
|
*/
|
|
@@ -279,9 +399,10 @@ export class ToolRegistry {
|
|
|
279
399
|
if (!tool.supportedLevels.includes(context.level)) {
|
|
280
400
|
continue;
|
|
281
401
|
}
|
|
282
|
-
// Pass 2: Ask tool if it's relevant
|
|
402
|
+
// Pass 2: Ask tool if it's relevant. A region capability declares no
|
|
403
|
+
// answer and has no toolbar presence, so it is never visible here.
|
|
283
404
|
try {
|
|
284
|
-
if (tool.isVisibleInContext(context)) {
|
|
405
|
+
if (tool.isVisibleInContext?.(context)) {
|
|
285
406
|
visible.push(tool);
|
|
286
407
|
}
|
|
287
408
|
}
|
|
@@ -306,6 +427,9 @@ export class ToolRegistry {
|
|
|
306
427
|
supportedLevels: tool.supportedLevels,
|
|
307
428
|
activation: tool.activation || "toolbar-toggle",
|
|
308
429
|
singletonScope: tool.singletonScope || null,
|
|
430
|
+
surfaces: tool.surfaces || [],
|
|
431
|
+
requiresAuthoredContent: Boolean(tool.requiresAuthoredContent),
|
|
432
|
+
contentDependencyDescription: tool.requiresAuthoredContent?.description ?? null,
|
|
309
433
|
}));
|
|
310
434
|
}
|
|
311
435
|
/**
|
|
@@ -331,28 +455,43 @@ export class ToolRegistry {
|
|
|
331
455
|
* Clear all registrations (useful for testing)
|
|
332
456
|
*/
|
|
333
457
|
clear() {
|
|
458
|
+
const toolIds = this.getAllToolIds();
|
|
459
|
+
if (toolIds.length === 0)
|
|
460
|
+
return;
|
|
334
461
|
this.tools.clear();
|
|
335
462
|
this.pnpIndex.clear();
|
|
463
|
+
this.emitChange({ kind: "clear", toolIds });
|
|
336
464
|
}
|
|
337
465
|
/**
|
|
338
466
|
* Configure global component overrides used by tool instance creation.
|
|
339
467
|
*/
|
|
340
468
|
setComponentOverrides(overrides) {
|
|
469
|
+
if (this.componentOverrides === overrides)
|
|
470
|
+
return;
|
|
341
471
|
this.componentOverrides = overrides;
|
|
472
|
+
this.emitChange({ kind: "component-overrides", toolIds: [] });
|
|
342
473
|
}
|
|
343
474
|
/**
|
|
344
475
|
* Register lazy module loaders by toolId.
|
|
345
476
|
* Toolbars call ensureToolModuleLoaded(toolId) before instance creation.
|
|
346
477
|
*/
|
|
347
478
|
setToolModuleLoaders(loaders) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
continue;
|
|
479
|
+
const entries = Object.entries(loaders).filter((entry) => entry[1] !== undefined);
|
|
480
|
+
for (const [toolId, loader] of entries) {
|
|
351
481
|
assertNonEmptyString(toolId, "tool module loader id");
|
|
352
482
|
if (typeof loader !== "function") {
|
|
353
483
|
throw new Error(`Invalid tool module loader for "${toolId}": expected a function.`);
|
|
354
484
|
}
|
|
485
|
+
}
|
|
486
|
+
const changedToolIds = [];
|
|
487
|
+
for (const [toolId, loader] of entries) {
|
|
488
|
+
if (this.moduleLoaders.get(toolId) === loader)
|
|
489
|
+
continue;
|
|
355
490
|
this.moduleLoaders.set(toolId, loader);
|
|
491
|
+
changedToolIds.push(toolId);
|
|
492
|
+
}
|
|
493
|
+
if (changedToolIds.length > 0) {
|
|
494
|
+
this.emitChange({ kind: "module-loaders", toolIds: changedToolIds });
|
|
356
495
|
}
|
|
357
496
|
}
|
|
358
497
|
/**
|
|
@@ -402,6 +541,12 @@ export class ToolRegistry {
|
|
|
402
541
|
if (!tool) {
|
|
403
542
|
throw new Error(`Tool '${toolId}' is not registered`);
|
|
404
543
|
}
|
|
544
|
+
if (typeof tool.renderToolbar !== "function") {
|
|
545
|
+
// Naming the activation rather than "renderToolbar is not a function":
|
|
546
|
+
// the caller's mistake is asking a surface capability for a toolbar
|
|
547
|
+
// button, and it is fixed by placement config, not by the registration.
|
|
548
|
+
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.`);
|
|
549
|
+
}
|
|
405
550
|
const mergedContext = {
|
|
406
551
|
...toolbarContext,
|
|
407
552
|
componentOverrides: {
|
|
@@ -411,4 +556,29 @@ export class ToolRegistry {
|
|
|
411
556
|
};
|
|
412
557
|
return tool.renderToolbar(context, mergedContext);
|
|
413
558
|
}
|
|
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, context) {
|
|
569
|
+
const tool = this.get(toolId);
|
|
570
|
+
if (!tool) {
|
|
571
|
+
throw new Error(`Tool '${toolId}' is not registered`);
|
|
572
|
+
}
|
|
573
|
+
if (typeof tool.renderSurface !== "function") {
|
|
574
|
+
throw new Error(`Tool '${toolId}' does not render into a host surface. Surface capabilities declare "surfaces" and implement "renderSurface".`);
|
|
575
|
+
}
|
|
576
|
+
return tool.renderSurface({
|
|
577
|
+
...context,
|
|
578
|
+
componentOverrides: {
|
|
579
|
+
...(this.componentOverrides || {}),
|
|
580
|
+
...(context.componentOverrides || {}),
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
}
|
|
414
584
|
}
|