@pie-players/pie-assessment-toolkit 0.3.64 → 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 +45 -17
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +6 -6
- 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 +5 -6
- package/dist/index.js +9 -4
- 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/ToolRegistry.d.ts +218 -8
- package/dist/services/ToolRegistry.js +124 -8
- package/dist/services/ToolkitCoordinator.d.ts +2 -1
- package/dist/services/ToolkitCoordinator.js +23 -6
- 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/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 +2 -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-3cppre9r.js +0 -51
- package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
- 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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition emit scheduler (PIE-885).
|
|
3
|
+
*
|
|
4
|
+
* `PieAssessmentToolkit.svelte` publishes the section composition to the
|
|
5
|
+
* players through exactly one path — the `composition-changed` event — and
|
|
6
|
+
* coalesces bursts of updates behind a one-shot latch so several changes
|
|
7
|
+
* within one frame produce a single emit.
|
|
8
|
+
*
|
|
9
|
+
* That latch used to be cleared only by a `requestAnimationFrame` callback,
|
|
10
|
+
* and the frame branch was chosen whenever `window.requestAnimationFrame`
|
|
11
|
+
* merely *existed* rather than when it was known to fire. In a document that
|
|
12
|
+
* never paints the callback never ran, the latch never cleared, and no
|
|
13
|
+
* `composition-changed` was ever dispatched: the section controller held a
|
|
14
|
+
* correct view model while the player kept its initial empty composition, so
|
|
15
|
+
* every `pie-section-player` route rendered no content at all. The permanent
|
|
16
|
+
* failure was in contexts with no compositor — headless browsers, hidden or
|
|
17
|
+
* offscreen tabs, agent and CI automation harnesses. A background tab only
|
|
18
|
+
* rendered late, because its pending frame becomes due on refocus.
|
|
19
|
+
*
|
|
20
|
+
* So the frame is raced against a deadline timer instead of trusted. Whichever
|
|
21
|
+
* arrives first releases the latch and flushes, and a non-painting document
|
|
22
|
+
* degrades to a slower render rather than a permanent blank. Svelte's own
|
|
23
|
+
* `tick()` races the same two primitives for the same reason.
|
|
24
|
+
*
|
|
25
|
+
* This scheduler owns the latch and both handles, which is what makes "the
|
|
26
|
+
* latch cannot stay set after a scheduled frame is cancelled or superseded"
|
|
27
|
+
* structural rather than a discipline every call site has to keep: releasing
|
|
28
|
+
* the latch and releasing the handles is one operation in one place.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately stateless across cycles. A cycle the timer wins does not switch
|
|
31
|
+
* the scheduler into a timer-preferring mode: that trades the deadline cost for
|
|
32
|
+
* a mode which never returns to frame alignment once a single frame is missed,
|
|
33
|
+
* and a slower render is the accepted outcome.
|
|
34
|
+
*
|
|
35
|
+
* NOT a public surface: internal to the toolkit and not exported from
|
|
36
|
+
* `runtime/engine.ts`. It exists as its own module so the race can be pinned in
|
|
37
|
+
* a unit test without mounting the toolkit CE — same rationale as
|
|
38
|
+
* `runtime/stage-emit-gate.ts`.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* How long to wait for a frame before the timer takes over.
|
|
42
|
+
*
|
|
43
|
+
* Six frames at 60fps, so on any normally painting document the frame still
|
|
44
|
+
* wins and emits stay paint-aligned. Chrome clamps timers in hidden tabs to
|
|
45
|
+
* ≥1s and can clamp harder under intensive throttling; that makes a hidden tab
|
|
46
|
+
* bounded-slow instead of blank, which is the point.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_FRAME_DEADLINE_MS = 100;
|
|
49
|
+
function pickFunction(override, ambient) {
|
|
50
|
+
if (typeof override === "function")
|
|
51
|
+
return override;
|
|
52
|
+
if (override === null)
|
|
53
|
+
return null;
|
|
54
|
+
return typeof ambient === "function" ? ambient : null;
|
|
55
|
+
}
|
|
56
|
+
export function createCompositionEmitScheduler(timing = {}) {
|
|
57
|
+
const frameDeadlineMs = timing.frameDeadlineMs ?? DEFAULT_FRAME_DEADLINE_MS;
|
|
58
|
+
let pending = false;
|
|
59
|
+
let pendingFlush = null;
|
|
60
|
+
let frameHandle = null;
|
|
61
|
+
let timerHandle = null;
|
|
62
|
+
// Kept from the arming call so a cancel is paired with the primitive that
|
|
63
|
+
// actually armed the handle.
|
|
64
|
+
let cancelFrame = null;
|
|
65
|
+
let clearTimer = null;
|
|
66
|
+
function release() {
|
|
67
|
+
pending = false;
|
|
68
|
+
if (frameHandle !== null) {
|
|
69
|
+
cancelFrame?.(frameHandle);
|
|
70
|
+
frameHandle = null;
|
|
71
|
+
}
|
|
72
|
+
if (timerHandle !== null) {
|
|
73
|
+
clearTimer?.(timerHandle);
|
|
74
|
+
timerHandle = null;
|
|
75
|
+
}
|
|
76
|
+
cancelFrame = null;
|
|
77
|
+
clearTimer = null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve one cycle. `firedFrom` names the side that arrived so its own
|
|
81
|
+
* handle is dropped rather than handed back to a cancel that has nothing
|
|
82
|
+
* left to cancel.
|
|
83
|
+
*
|
|
84
|
+
* The latch is released before the flush callback runs, so a re-entrant
|
|
85
|
+
* `schedule()` from inside the flush arms a fresh cycle instead of being
|
|
86
|
+
* swallowed. The pre-fix code cleared its latch first for the same reason.
|
|
87
|
+
*/
|
|
88
|
+
function resolve(firedFrom) {
|
|
89
|
+
if (!pending)
|
|
90
|
+
return;
|
|
91
|
+
if (firedFrom === "frame")
|
|
92
|
+
frameHandle = null;
|
|
93
|
+
if (firedFrom === "timer")
|
|
94
|
+
timerHandle = null;
|
|
95
|
+
const flushNow = pendingFlush;
|
|
96
|
+
pendingFlush = null;
|
|
97
|
+
release();
|
|
98
|
+
flushNow?.();
|
|
99
|
+
}
|
|
100
|
+
function schedule(flush) {
|
|
101
|
+
pendingFlush = flush;
|
|
102
|
+
if (pending)
|
|
103
|
+
return;
|
|
104
|
+
pending = true;
|
|
105
|
+
const ambient = globalThis;
|
|
106
|
+
const requestFrame = pickFunction(timing.requestFrame, ambient.requestAnimationFrame);
|
|
107
|
+
cancelFrame = pickFunction(timing.cancelFrame, ambient.cancelAnimationFrame);
|
|
108
|
+
const setTimer = pickFunction(timing.setTimer, ambient.setTimeout);
|
|
109
|
+
clearTimer = pickFunction(timing.clearTimer, ambient.clearTimeout);
|
|
110
|
+
if (requestFrame) {
|
|
111
|
+
const handle = requestFrame(() => resolve("frame"));
|
|
112
|
+
// A primitive that calls back synchronously has already flushed and
|
|
113
|
+
// released; arming anything else would leave a stray handle behind.
|
|
114
|
+
if (!pending)
|
|
115
|
+
return;
|
|
116
|
+
frameHandle = handle;
|
|
117
|
+
// The frame is not trusted to arrive. Without a timer alongside it, a
|
|
118
|
+
// document that never paints leaves the latch set forever.
|
|
119
|
+
if (setTimer) {
|
|
120
|
+
const deadlineHandle = setTimer(() => resolve("timer"), frameDeadlineMs);
|
|
121
|
+
if (pending)
|
|
122
|
+
timerHandle = deadlineHandle;
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// No frame primitive at all (SSR, non-DOM test environments). Preserve
|
|
127
|
+
// the microtask timing those hosts already had.
|
|
128
|
+
const queueMicrotaskFn = pickFunction(timing.queueMicrotaskFn, ambient.queueMicrotask);
|
|
129
|
+
if (queueMicrotaskFn) {
|
|
130
|
+
queueMicrotaskFn(() => resolve("microtask"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (setTimer) {
|
|
134
|
+
const deadlineHandle = setTimer(() => resolve("timer"), 0);
|
|
135
|
+
if (pending)
|
|
136
|
+
timerHandle = deadlineHandle;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Nothing to defer with. A synchronous flush is strictly better than
|
|
140
|
+
// dropping the composition on the floor.
|
|
141
|
+
resolve("microtask");
|
|
142
|
+
}
|
|
143
|
+
function cancel() {
|
|
144
|
+
if (!pending)
|
|
145
|
+
return;
|
|
146
|
+
pendingFlush = null;
|
|
147
|
+
release();
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
schedule,
|
|
151
|
+
cancel,
|
|
152
|
+
isPending: () => pending,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -120,7 +120,7 @@ export declare function resolveRuntime(args: {
|
|
|
120
120
|
createSectionController: unknown;
|
|
121
121
|
isolation: string;
|
|
122
122
|
env: Record<string, unknown>;
|
|
123
|
-
toolConfigStrictness: "off" | "
|
|
123
|
+
toolConfigStrictness: "off" | "warn" | "error";
|
|
124
124
|
onFrameworkError: FrameworkErrorHandler | undefined;
|
|
125
125
|
onStageChange: StageChangeHandler | undefined;
|
|
126
126
|
onLoadingComplete: LoadingCompleteHandler | undefined;
|
|
@@ -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
|
}
|
|
@@ -29,7 +29,8 @@ import type { SREMathSpeechOptions } from "./tts/math-speech.js";
|
|
|
29
29
|
import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
30
30
|
import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
31
31
|
import type { TTSToolProviderConfig } from "./tool-providers/index.js";
|
|
32
|
-
import
|
|
32
|
+
import { ToolRegistry } from "./ToolRegistry.js";
|
|
33
|
+
import type { ResolvedToolContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap } from "./ToolRegistry.js";
|
|
33
34
|
import { type FeaturePolicyDecision, type PnpEnforcementMode, type PolicySource, type ResolvedEngineInputs, type ToolPolicyChangeListener, type ToolPolicyDecision, type ToolPolicyDecisionRequest } from "../policy/engine.js";
|
|
34
35
|
import type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionSessionPersistenceStrategy, SectionPersistenceFactoryDefaults } from "./section-controller-types.js";
|
|
35
36
|
export type { SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionControllerLoadedRenderable, SectionSessionPersistenceConfig, SectionSessionPersistenceStrategy, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, } from "./section-controller-types.js";
|