@pie-players/pie-assessment-toolkit 0.3.66 → 0.3.68
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 +1 -1
- package/dist/attempt/AssessmentSession.d.ts +7 -27
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-38mhtjsq.js +51 -0
- package/dist/components/chunks/ItemToolBar-9ymm7pd1.js +46 -0
- package/dist/components/item-toolbar-element.js +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +1 -0
- package/dist/components/section-toolbar-element.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +28 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +12 -0
- package/dist/policy/core/ToolPolicyEngine.js +28 -2
- package/dist/policy/core/feature-decision.d.ts +28 -2
- package/dist/policy/core/feature-decision.js +35 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/engine.js +1 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
- package/dist/runtime/SectionRuntimeEngine.js +66 -1
- package/dist/runtime/core/engine-resolver.d.ts +25 -1
- package/dist/runtime/registration-events.d.ts +52 -0
- package/dist/runtime/registration-events.js +2 -0
- package/dist/services/AccessibilityCatalogResolver.js +21 -5
- package/dist/services/I18nService.d.ts +28 -100
- package/dist/services/I18nService.js +43 -233
- package/dist/services/TTSService.d.ts +12 -0
- package/dist/services/TTSService.js +18 -17
- package/dist/services/ToolRegistry.d.ts +86 -2
- package/dist/services/ToolRegistry.js +30 -0
- package/dist/services/ToolkitCoordinator.d.ts +39 -37
- package/dist/services/ToolkitCoordinator.js +40 -0
- package/dist/services/audio-handoff.d.ts +39 -0
- package/dist/services/audio-handoff.js +58 -0
- package/dist/services/catalog-media.d.ts +35 -4
- package/dist/services/catalog-media.js +92 -3
- package/dist/services/framework-error.d.ts +15 -1
- package/dist/services/interfaces.d.ts +28 -0
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +218 -7
- package/dist/services/selection-action.d.ts +49 -0
- package/dist/services/selection-action.js +10 -0
- package/dist/services/spoken-audio-cards.js +5 -1
- package/dist/services/tool-context.d.ts +6 -5
- package/dist/services/tool-context.js +197 -152
- package/dist/services/tool-icons.d.ts +18 -0
- package/dist/services/tool-icons.js +31 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +6 -5
- package/dist/services/tool-request.d.ts +106 -0
- package/dist/services/tool-request.js +127 -0
- package/dist/services/toolbar-items.d.ts +6 -0
- package/dist/tools/client.d.ts +0 -1
- package/dist/tools/client.js +0 -2
- package/dist/tools/content-capability-resolution.d.ts +106 -0
- package/dist/tools/content-capability-resolution.js +136 -0
- package/dist/tools/internal.d.ts +8 -0
- package/dist/tools/internal.js +8 -0
- package/dist/tools/tool-surface-host.d.ts +57 -0
- package/dist/tools/tool-surface-host.js +610 -0
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
- package/dist/tools/calculators/desmos-provider.d.ts +0 -46
- package/dist/tools/calculators/desmos-provider.js +0 -393
|
@@ -113,6 +113,18 @@ export declare class ToolPolicyEngine {
|
|
|
113
113
|
* {@link FeaturePolicyDecision.assessmentBound}.
|
|
114
114
|
*/
|
|
115
115
|
decideFeature(featureId: string): FeaturePolicyDecision;
|
|
116
|
+
/**
|
|
117
|
+
* The host gates that hold for a feature id, or `null` when none fires.
|
|
118
|
+
*
|
|
119
|
+
* `policy.allowed` / `policy.blocked` name capabilities, not placements, so
|
|
120
|
+
* they are the one part of the host pipeline that is meaningful without a
|
|
121
|
+
* placement level — and the only lever a host has over a capability that
|
|
122
|
+
* renders as its own surface, since a `region` capability is rejected from
|
|
123
|
+
* `tools.placement` by configuration validation. `provider-disabled` and
|
|
124
|
+
* `placement-membership` are deliberately not applied: both are statements
|
|
125
|
+
* about a toolbar the feature was never on.
|
|
126
|
+
*/
|
|
127
|
+
private hostFeatureGate;
|
|
116
128
|
/**
|
|
117
129
|
* Convenience wrapper for hosts that just want the visible tool
|
|
118
130
|
* IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* PR 1 ships the engine *without callers*. PR 2 wires it into
|
|
17
17
|
* `ToolkitCoordinator`. PR 3 switches `<pie-item-toolbar>` over.
|
|
18
18
|
*/
|
|
19
|
-
import { normalizeToolsConfig } from "../../services/tools-config-normalizer.js";
|
|
20
|
-
import { interpretFeatureResult } from "./feature-decision.js";
|
|
19
|
+
import { normalizeToolList, normalizeToolsConfig, } from "../../services/tools-config-normalizer.js";
|
|
20
|
+
import { hostFeatureDenial, interpretFeatureResult, } from "./feature-decision.js";
|
|
21
21
|
import { composeDecision } from "./compose-decision.js";
|
|
22
22
|
import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
|
|
23
23
|
import { PnpPolicySource } from "../sources/PnpPolicySource.js";
|
|
@@ -111,6 +111,9 @@ export class ToolPolicyEngine {
|
|
|
111
111
|
*/
|
|
112
112
|
decideFeature(featureId) {
|
|
113
113
|
this.assertNotDisposed();
|
|
114
|
+
const hostDenial = this.hostFeatureGate(featureId);
|
|
115
|
+
if (hostDenial)
|
|
116
|
+
return hostDenial;
|
|
114
117
|
return interpretFeatureResult(featureId, this.pnpPolicySource.resolveFeature(featureId, {
|
|
115
118
|
assessment: this.assessment ?? undefined,
|
|
116
119
|
currentItemRef: this.currentItemRef ?? undefined,
|
|
@@ -120,6 +123,29 @@ export class ToolPolicyEngine {
|
|
|
120
123
|
// The engine can.
|
|
121
124
|
{ assessmentBound: this.assessment !== null });
|
|
122
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The host gates that hold for a feature id, or `null` when none fires.
|
|
128
|
+
*
|
|
129
|
+
* `policy.allowed` / `policy.blocked` name capabilities, not placements, so
|
|
130
|
+
* they are the one part of the host pipeline that is meaningful without a
|
|
131
|
+
* placement level — and the only lever a host has over a capability that
|
|
132
|
+
* renders as its own surface, since a `region` capability is rejected from
|
|
133
|
+
* `tools.placement` by configuration validation. `provider-disabled` and
|
|
134
|
+
* `placement-membership` are deliberately not applied: both are statements
|
|
135
|
+
* about a toolbar the feature was never on.
|
|
136
|
+
*/
|
|
137
|
+
hostFeatureGate(featureId) {
|
|
138
|
+
const context = { assessmentBound: this.assessment !== null };
|
|
139
|
+
const blocked = normalizeToolList(this.tools.policy.blocked);
|
|
140
|
+
if (blocked.includes(featureId)) {
|
|
141
|
+
return hostFeatureDenial(featureId, "host-blocked", blocked, context);
|
|
142
|
+
}
|
|
143
|
+
const allowed = normalizeToolList(this.tools.policy.allowed);
|
|
144
|
+
if (allowed.length > 0 && !allowed.includes(featureId)) {
|
|
145
|
+
return hostFeatureDenial(featureId, "host-allowlist", allowed, context);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
123
149
|
/**
|
|
124
150
|
* Convenience wrapper for hosts that just want the visible tool
|
|
125
151
|
* IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
import type { PnpPolicyResult } from "../sources/PnpPolicySource.js";
|
|
24
24
|
import type { PnpPolicySourceRule } from "./policy-source-tag.js";
|
|
25
25
|
import type { ToolPolicyResolutionDecision, ToolPolicySourceType } from "./provenance.js";
|
|
26
|
+
/**
|
|
27
|
+
* A feature verdict comes from one of the six PNP precedence levels, or from a
|
|
28
|
+
* host gate that never reaches them: `tools.policy.blocked` and a non-empty
|
|
29
|
+
* `tools.policy.allowed` are absolute for the id they name, exactly as they are
|
|
30
|
+
* on the placement-scoped path.
|
|
31
|
+
*/
|
|
32
|
+
export type FeaturePolicyRule = PnpPolicySourceRule | "host-allowlist" | "host-blocked";
|
|
26
33
|
export interface FeaturePolicyDecision {
|
|
27
34
|
/** The PNP/AfA support id that was evaluated (e.g. `"signLanguage"`). */
|
|
28
35
|
featureId: string;
|
|
@@ -34,8 +41,8 @@ export interface FeaturePolicyDecision {
|
|
|
34
41
|
granted: boolean;
|
|
35
42
|
action: ToolPolicyResolutionDecision["action"];
|
|
36
43
|
/** Which precedence rule produced the verdict. */
|
|
37
|
-
rule:
|
|
38
|
-
precedence: 1 | 2 | 3 | 4 | 5 | 6;
|
|
44
|
+
rule: FeaturePolicyRule;
|
|
45
|
+
precedence: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
39
46
|
sourceType: ToolPolicySourceType;
|
|
40
47
|
/** Human-readable explanation, suitable for a policy debugger. */
|
|
41
48
|
reason: string;
|
|
@@ -74,6 +81,25 @@ export interface FeatureDecisionContext {
|
|
|
74
81
|
/** Whether the engine has an assessment bound. */
|
|
75
82
|
assessmentBound: boolean;
|
|
76
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* A host gate denied the feature outright, so no policy source was consulted.
|
|
86
|
+
*
|
|
87
|
+
* `precedence: 0` and the two `host-*` rules are the same values
|
|
88
|
+
* `composeDecision(...)` records for the placement-scoped path, so a debugger
|
|
89
|
+
* reads one vocabulary for both. `assessmentBound` is still reported: a host
|
|
90
|
+
* blocklist is a verdict whether or not an assessment was bound, and the flag
|
|
91
|
+
* only ever qualifies a *policy* denial.
|
|
92
|
+
*/
|
|
93
|
+
export declare function hostFeatureDenial(featureId: string, rule: "host-allowlist" | "host-blocked", hostValue: readonly string[], context: FeatureDecisionContext): FeaturePolicyDecision;
|
|
94
|
+
/**
|
|
95
|
+
* Whether a host gate produced the verdict, rather than a policy source.
|
|
96
|
+
*
|
|
97
|
+
* The distinction is load-bearing for a content-dependent capability that
|
|
98
|
+
* declares `resolvesWithoutGrant`: an absent grant is a case it is allowed to
|
|
99
|
+
* answer from the content alone, while a host denial is the host's off switch
|
|
100
|
+
* and nothing may reopen it.
|
|
101
|
+
*/
|
|
102
|
+
export declare function isHostDeniedFeature(decision: Pick<FeaturePolicyDecision, "rule"> | null | undefined): boolean;
|
|
77
103
|
/**
|
|
78
104
|
* Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
|
|
79
105
|
*
|
|
@@ -29,6 +29,41 @@
|
|
|
29
29
|
* a precedence level that does not exist.
|
|
30
30
|
*/
|
|
31
31
|
const unboundAssessmentReason = (featureId) => `No assessment is bound, so no policy source could grant "${featureId}"`;
|
|
32
|
+
/**
|
|
33
|
+
* A host gate denied the feature outright, so no policy source was consulted.
|
|
34
|
+
*
|
|
35
|
+
* `precedence: 0` and the two `host-*` rules are the same values
|
|
36
|
+
* `composeDecision(...)` records for the placement-scoped path, so a debugger
|
|
37
|
+
* reads one vocabulary for both. `assessmentBound` is still reported: a host
|
|
38
|
+
* blocklist is a verdict whether or not an assessment was bound, and the flag
|
|
39
|
+
* only ever qualifies a *policy* denial.
|
|
40
|
+
*/
|
|
41
|
+
export function hostFeatureDenial(featureId, rule, hostValue, context) {
|
|
42
|
+
return {
|
|
43
|
+
featureId,
|
|
44
|
+
granted: false,
|
|
45
|
+
action: "block",
|
|
46
|
+
rule,
|
|
47
|
+
precedence: 0,
|
|
48
|
+
sourceType: "host",
|
|
49
|
+
reason: rule === "host-blocked"
|
|
50
|
+
? "Listed in tools.policy.blocked"
|
|
51
|
+
: `Not listed in tools.policy.allowed (${hostValue.join(", ")})`,
|
|
52
|
+
required: false,
|
|
53
|
+
assessmentBound: context.assessmentBound,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a host gate produced the verdict, rather than a policy source.
|
|
58
|
+
*
|
|
59
|
+
* The distinction is load-bearing for a content-dependent capability that
|
|
60
|
+
* declares `resolvesWithoutGrant`: an absent grant is a case it is allowed to
|
|
61
|
+
* answer from the content alone, while a host denial is the host's off switch
|
|
62
|
+
* and nothing may reopen it.
|
|
63
|
+
*/
|
|
64
|
+
export function isHostDeniedFeature(decision) {
|
|
65
|
+
return (decision?.rule === "host-blocked" || decision?.rule === "host-allowlist");
|
|
66
|
+
}
|
|
32
67
|
/**
|
|
33
68
|
* Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
|
|
34
69
|
*
|
package/dist/policy/engine.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export { ToolPolicyEngine, type PnpEnforcementMode, type ResolvedEngineInputs, type ToolPolicyChangeEvent, type ToolPolicyChangeListener, type ToolPolicyEngineArgs, type ToolPolicyEngineInputs, } from "./core/ToolPolicyEngine.js";
|
|
23
23
|
export { TOOL_POLICY_ENGINE_KEY, type ToolPolicyEngineContext, } from "./core/engine-context.js";
|
|
24
|
-
export type
|
|
24
|
+
export { isHostDeniedFeature, type FeaturePolicyDecision, type FeaturePolicyRule, } from "./core/feature-decision.js";
|
|
25
25
|
export type { RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, } from "./core/decision-types.js";
|
|
26
26
|
export type { PolicySource, PolicySourceDecisionContext, PolicySourceProvenanceEntry, PolicySourceResult, } from "./core/PolicySource.js";
|
|
27
27
|
export type { PolicySourceTag, PnpPolicySourceRule, PnpPolicySourceTag, CustomPolicySourceTag, } from "./core/policy-source-tag.js";
|
package/dist/policy/engine.js
CHANGED
|
@@ -48,6 +48,7 @@ import type { SectionEngineInput } from "./core/engine-input.js";
|
|
|
48
48
|
import type { SectionEngineOutput } from "./core/engine-output.js";
|
|
49
49
|
import { type EffectiveRuntime, type RuntimeInputs } from "./core/engine-resolver.js";
|
|
50
50
|
import { type SectionEngineState } from "./core/engine-state.js";
|
|
51
|
+
import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
|
|
51
52
|
import type { RuntimeRegistrationDetail } from "./registration-events.js";
|
|
52
53
|
import { RuntimeRegistry } from "./RuntimeRegistry.js";
|
|
53
54
|
/**
|
|
@@ -95,6 +96,27 @@ interface RuntimeController extends SectionControllerHandle {
|
|
|
95
96
|
} | null;
|
|
96
97
|
subscribe?: (listener: (event: SectionControllerEvent) => void) => () => void;
|
|
97
98
|
navigateToItem?: (index: number) => unknown;
|
|
99
|
+
attachMediaTimeSource?: (source: MediaTimeSource, options?: {
|
|
100
|
+
origin?: "native-adapter" | "host";
|
|
101
|
+
renderableId?: string;
|
|
102
|
+
}) => void;
|
|
103
|
+
detachMediaTimeSource?: (options?: {
|
|
104
|
+
origin?: "native-adapter" | "host";
|
|
105
|
+
}) => void;
|
|
106
|
+
pauseMediaForCompetingAudio?: () => boolean;
|
|
107
|
+
}
|
|
108
|
+
/** What a media-time-source registration reaching the engine has to say. */
|
|
109
|
+
export interface SectionRuntimeMediaTimeSourceAction {
|
|
110
|
+
renderableId: string;
|
|
111
|
+
action: "attach" | "detach";
|
|
112
|
+
source?: MediaTimeSource;
|
|
113
|
+
origin?: "native-adapter" | "host";
|
|
114
|
+
}
|
|
115
|
+
/** What a formative action reaching the engine has to say. */
|
|
116
|
+
export interface SectionRuntimeFormativeAction {
|
|
117
|
+
itemId: string;
|
|
118
|
+
action: "check" | "retry";
|
|
119
|
+
outcomes?: unknown[];
|
|
98
120
|
}
|
|
99
121
|
/**
|
|
100
122
|
* Initialize args used by the toolkit CE to resolve the coordinator-backed
|
|
@@ -109,6 +131,14 @@ interface EngineInitArgs {
|
|
|
109
131
|
attemptId?: string;
|
|
110
132
|
createDefaultController: () => Promise<RuntimeController> | RuntimeController;
|
|
111
133
|
onCompositionChanged?: (composition: unknown) => void;
|
|
134
|
+
/**
|
|
135
|
+
* The controller event that caused a republish, for the few events the toolkit
|
|
136
|
+
* has to act on rather than merely propagate — a timed-media policy that cannot
|
|
137
|
+
* be enforced becomes a framework warning, and malformed cue data becomes a
|
|
138
|
+
* framework error. Every other event reaches hosts through the composition
|
|
139
|
+
* republish and the coordinator's own subscriptions.
|
|
140
|
+
*/
|
|
141
|
+
onControllerEvent?: (event: SectionControllerEvent) => void;
|
|
112
142
|
}
|
|
113
143
|
/**
|
|
114
144
|
* Engine-side `attachHost` args. The host element, framework-error bus,
|
|
@@ -273,6 +303,32 @@ export declare class SectionRuntimeEngine {
|
|
|
273
303
|
}): void;
|
|
274
304
|
updateItemSession(itemId: string, session: unknown): unknown;
|
|
275
305
|
navigateToItem(index: number): unknown;
|
|
306
|
+
/**
|
|
307
|
+
* Route a learner's formative action to the controller.
|
|
308
|
+
*
|
|
309
|
+
* Canonicalized here for the same reason `updateItemSession` is: the runtime
|
|
310
|
+
* id a card dispatches with is not necessarily the identifier the controller
|
|
311
|
+
* keys state by.
|
|
312
|
+
*/
|
|
313
|
+
handleFormativeAction(action: SectionRuntimeFormativeAction): void;
|
|
314
|
+
/**
|
|
315
|
+
* Bind or release the section's Media Time Source.
|
|
316
|
+
*
|
|
317
|
+
* A pass-through, like `handleFormativeAction`: the engine routes, the
|
|
318
|
+
* controller decides. `renderableId` travels with the attach because whether the
|
|
319
|
+
* registering renderable is the one `stimulusRef` names is the controller's call
|
|
320
|
+
* — it validated `stimulusRef` in the first place.
|
|
321
|
+
*/
|
|
322
|
+
handleMediaTimeSource(action: SectionRuntimeMediaTimeSourceAction): void;
|
|
323
|
+
/**
|
|
324
|
+
* Silence media audio because something else is about to speak.
|
|
325
|
+
*
|
|
326
|
+
* A pass-through like `handleMediaTimeSource`, and for the same reason: which
|
|
327
|
+
* source is authoritative and whether it reports `canPause` are the controller's
|
|
328
|
+
* to know. Returns whether media audio is now silent, or `true` where there is
|
|
329
|
+
* no timed-media controller to ask — nothing is playing.
|
|
330
|
+
*/
|
|
331
|
+
requestMediaPauseForCompetingAudio(): boolean;
|
|
276
332
|
persist(): Promise<void>;
|
|
277
333
|
hydrate(): Promise<void>;
|
|
278
334
|
getRegistry(): RuntimeRegistry;
|
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
import { SectionEngineAdapter, } from "./adapter/SectionEngineAdapter.js";
|
|
42
42
|
import { resolveSectionEngineRuntimeState, } from "./core/engine-resolver.js";
|
|
43
43
|
import { createInitialEngineState, } from "./core/engine-state.js";
|
|
44
|
+
import { createPieLogger, isGlobalDebugEnabled, } from "@pie-players/pie-players-shared";
|
|
44
45
|
import { RuntimeRegistry } from "./RuntimeRegistry.js";
|
|
45
46
|
import { createRuntimeId } from "./runtime-id.js";
|
|
47
|
+
const logger = createPieLogger("section-runtime-engine", () => isGlobalDebugEnabled());
|
|
46
48
|
export class SectionRuntimeEngine {
|
|
47
49
|
registry = new RuntimeRegistry();
|
|
48
50
|
runtimeId = createRuntimeId("section-engine");
|
|
@@ -209,7 +211,17 @@ export class SectionRuntimeEngine {
|
|
|
209
211
|
this.replayRegisteredShellsIntoController(resolved);
|
|
210
212
|
args.onCompositionChanged?.(resolved.getCompositionModel?.());
|
|
211
213
|
this.unsubscribeController =
|
|
212
|
-
resolved.subscribe?.(() => {
|
|
214
|
+
resolved.subscribe?.((event) => {
|
|
215
|
+
// Isolated deliberately: the controller's emit loop catches per
|
|
216
|
+
// listener, so a diagnostic handler that throws would take the
|
|
217
|
+
// composition republish down with it and every cue and Try would stop
|
|
218
|
+
// reaching the cards — a failure with no symptom except a warning.
|
|
219
|
+
try {
|
|
220
|
+
args.onControllerEvent?.(event);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
logger.warn("onControllerEvent handler threw", error);
|
|
224
|
+
}
|
|
213
225
|
args.onCompositionChanged?.(resolved.getCompositionModel?.());
|
|
214
226
|
}) || null;
|
|
215
227
|
}
|
|
@@ -333,6 +345,59 @@ export class SectionRuntimeEngine {
|
|
|
333
345
|
navigateToItem(index) {
|
|
334
346
|
return this.controller?.navigateToItem?.(index) ?? null;
|
|
335
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Route a learner's formative action to the controller.
|
|
350
|
+
*
|
|
351
|
+
* Canonicalized here for the same reason `updateItemSession` is: the runtime
|
|
352
|
+
* id a card dispatches with is not necessarily the identifier the controller
|
|
353
|
+
* keys state by.
|
|
354
|
+
*/
|
|
355
|
+
handleFormativeAction(action) {
|
|
356
|
+
if (!action?.itemId)
|
|
357
|
+
return;
|
|
358
|
+
const canonicalId = this.getCanonicalItemId(action.itemId);
|
|
359
|
+
if (action.action === "retry") {
|
|
360
|
+
this.controller?.retryFormativeItem?.({ itemId: canonicalId });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.controller?.recordFormativeTry?.({
|
|
364
|
+
itemId: canonicalId,
|
|
365
|
+
outcomes: action.outcomes,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Bind or release the section's Media Time Source.
|
|
370
|
+
*
|
|
371
|
+
* A pass-through, like `handleFormativeAction`: the engine routes, the
|
|
372
|
+
* controller decides. `renderableId` travels with the attach because whether the
|
|
373
|
+
* registering renderable is the one `stimulusRef` names is the controller's call
|
|
374
|
+
* — it validated `stimulusRef` in the first place.
|
|
375
|
+
*/
|
|
376
|
+
handleMediaTimeSource(action) {
|
|
377
|
+
if (action?.action === "detach") {
|
|
378
|
+
this.controller?.detachMediaTimeSource?.({
|
|
379
|
+
origin: action.origin ?? "host",
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!action?.source)
|
|
384
|
+
return;
|
|
385
|
+
this.controller?.attachMediaTimeSource?.(action.source, {
|
|
386
|
+
origin: action.origin ?? "host",
|
|
387
|
+
renderableId: action.renderableId,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Silence media audio because something else is about to speak.
|
|
392
|
+
*
|
|
393
|
+
* A pass-through like `handleMediaTimeSource`, and for the same reason: which
|
|
394
|
+
* source is authoritative and whether it reports `canPause` are the controller's
|
|
395
|
+
* to know. Returns whether media audio is now silent, or `true` where there is
|
|
396
|
+
* no timed-media controller to ask — nothing is playing.
|
|
397
|
+
*/
|
|
398
|
+
requestMediaPauseForCompetingAudio() {
|
|
399
|
+
return this.controller?.pauseMediaForCompetingAudio?.() ?? true;
|
|
400
|
+
}
|
|
336
401
|
async persist() {
|
|
337
402
|
await this.controller?.persist?.();
|
|
338
403
|
}
|
|
@@ -68,6 +68,18 @@ export type RuntimeConfig = {
|
|
|
68
68
|
* markup (the default). Purely visual — no engine effect.
|
|
69
69
|
*/
|
|
70
70
|
ndsIcons?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Interface locale: a BCP-47 tag naming the language the player renders its own
|
|
73
|
+
* UI in. Purely presentational — no engine effect.
|
|
74
|
+
*
|
|
75
|
+
* Distinct from content language, which describes the authored item and
|
|
76
|
+
* travels on `env`. QTI 3's implementation guide states the independence
|
|
77
|
+
* directly: a candidate may choose an interface language which may or may not
|
|
78
|
+
* also be the language of the content.
|
|
79
|
+
*
|
|
80
|
+
* Unset renders `en-US`. POSIX (`nl_NL`) and bare (`nl`) forms both resolve.
|
|
81
|
+
*/
|
|
82
|
+
locale?: string;
|
|
71
83
|
toolConfigStrictness?: ToolConfigStrictness;
|
|
72
84
|
onFrameworkError?: FrameworkErrorHandler;
|
|
73
85
|
onStageChange?: StageChangeHandler;
|
|
@@ -120,7 +132,7 @@ export declare function resolveRuntime(args: {
|
|
|
120
132
|
createSectionController: unknown;
|
|
121
133
|
isolation: string;
|
|
122
134
|
env: Record<string, unknown>;
|
|
123
|
-
toolConfigStrictness: "off" | "
|
|
135
|
+
toolConfigStrictness: "off" | "error" | "warn";
|
|
124
136
|
onFrameworkError: FrameworkErrorHandler | undefined;
|
|
125
137
|
onStageChange: StageChangeHandler | undefined;
|
|
126
138
|
onLoadingComplete: LoadingCompleteHandler | undefined;
|
|
@@ -134,6 +146,18 @@ export declare function resolveRuntime(args: {
|
|
|
134
146
|
* markup (the default). Purely visual — no engine effect.
|
|
135
147
|
*/
|
|
136
148
|
ndsIcons?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Interface locale: a BCP-47 tag naming the language the player renders its own
|
|
151
|
+
* UI in. Purely presentational — no engine effect.
|
|
152
|
+
*
|
|
153
|
+
* Distinct from content language, which describes the authored item and
|
|
154
|
+
* travels on `env`. QTI 3's implementation guide states the independence
|
|
155
|
+
* directly: a candidate may choose an interface language which may or may not
|
|
156
|
+
* also be the language of the content.
|
|
157
|
+
*
|
|
158
|
+
* Unset renders `en-US`. POSIX (`nl_NL`) and bare (`nl`) forms both resolve.
|
|
159
|
+
*/
|
|
160
|
+
locale?: string;
|
|
137
161
|
};
|
|
138
162
|
/**
|
|
139
163
|
* Effective runtime returned by `resolveRuntime`. The shape is exposed
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
|
|
1
2
|
export declare const PIE_REGISTER_EVENT = "pie-register";
|
|
2
3
|
export declare const PIE_UNREGISTER_EVENT = "pie-unregister";
|
|
3
4
|
export declare const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed";
|
|
4
5
|
export declare const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
|
|
5
6
|
export declare const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
|
|
6
7
|
export declare const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
|
|
8
|
+
export declare const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
|
|
9
|
+
export declare const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
|
|
7
10
|
export type RuntimeRegistrationKind = "item" | "passage";
|
|
8
11
|
export interface RuntimeRegistrationDetail {
|
|
9
12
|
kind: RuntimeRegistrationKind;
|
|
@@ -35,3 +38,52 @@ export interface InternalItemPlayerErrorDetail {
|
|
|
35
38
|
contentKind?: string;
|
|
36
39
|
error: unknown;
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A learner's formative action, dispatched by the component that owns the
|
|
43
|
+
* control and the item player node — the only place that can call
|
|
44
|
+
* `provideScore()`. It reports outcomes rather than interpreting them; the
|
|
45
|
+
* section controller derives correctness and owns the state.
|
|
46
|
+
*
|
|
47
|
+
* `outcomes` is the array `pie-item-player.provideScore()` returned, verbatim,
|
|
48
|
+
* including the `undefined` slots it leaves for models with no element or
|
|
49
|
+
* controller.
|
|
50
|
+
*/
|
|
51
|
+
export interface InternalFormativeActionDetail {
|
|
52
|
+
itemId: string;
|
|
53
|
+
canonicalItemId?: string;
|
|
54
|
+
action: "check" | "retry";
|
|
55
|
+
outcomes?: unknown[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A Media Time Source becoming available or going away.
|
|
59
|
+
*
|
|
60
|
+
* The one seam through which a timed-media section reaches media, and
|
|
61
|
+
* deliberately the *only* one: the stimulus card dispatches this with a native
|
|
62
|
+
* `<video>` adapter, and a host wrapping a third-party player dispatches the same
|
|
63
|
+
* event with its own port. One code path, two producers — which is what keeps
|
|
64
|
+
* "a host can supply its own media element without shipping a PIE element" true
|
|
65
|
+
* rather than aspirational.
|
|
66
|
+
*
|
|
67
|
+
* `source` carries a live object, not serializable data. That is fine and
|
|
68
|
+
* intended: this event never crosses a realm, exactly like the element reference
|
|
69
|
+
* on `pie-register`.
|
|
70
|
+
*/
|
|
71
|
+
export interface InternalMediaTimeSourceDetail {
|
|
72
|
+
/** The renderable that owns the media, for matching against `stimulusRef`. */
|
|
73
|
+
renderableId: string;
|
|
74
|
+
action: "attach" | "detach";
|
|
75
|
+
source?: MediaTimeSource;
|
|
76
|
+
/**
|
|
77
|
+
* Which producer this came from. `"native-adapter"` is the stimulus card
|
|
78
|
+
* wrapping a media element it found in its own subtree; anything else is a host
|
|
79
|
+
* wiring its own player, and omitting the field reads as `"host"` because a
|
|
80
|
+
* caller constructing this event by hand is one.
|
|
81
|
+
*
|
|
82
|
+
* Load-bearing for precedence: the card re-runs its discovery whenever its
|
|
83
|
+
* content changes, so without this a host that supplied a third-party port would
|
|
84
|
+
* have it silently replaced by the native element mid-session — and the
|
|
85
|
+
* capabilities would flip back with it, which is exactly the "appears to enforce"
|
|
86
|
+
* failure this contract exists to prevent.
|
|
87
|
+
*/
|
|
88
|
+
origin?: "native-adapter" | "host";
|
|
89
|
+
}
|
|
@@ -4,3 +4,5 @@ export const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed
|
|
|
4
4
|
export const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
|
|
5
5
|
export const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
|
|
6
6
|
export const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
|
|
7
|
+
export const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
|
|
8
|
+
export const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { languageTagLookupSequence, normalizeLanguageTag, } from "@pie-players/pie-players-shared/i18n/language-tags";
|
|
1
2
|
import { catalogOwnerContextFor, collectOwnerCatalogRegistrations, } from "./catalog-owner.js";
|
|
2
3
|
import { sanitizeSsmlString } from "./SSMLExtractor.js";
|
|
3
4
|
/**
|
|
@@ -537,14 +538,25 @@ export class AccessibilityCatalogResolver {
|
|
|
537
538
|
*/
|
|
538
539
|
findMatchingCard(catalog, options) {
|
|
539
540
|
const { type, language, useFallback = true, form } = options;
|
|
540
|
-
// Language rungs, most specific first: requested language, then the
|
|
541
|
-
// language, then any.
|
|
541
|
+
// Language rungs, most specific first: the requested language, then the
|
|
542
|
+
// default language, then any.
|
|
543
|
+
//
|
|
544
|
+
// Each requested tag expands into its RFC 4647 lookup sequence, so `es-MX`
|
|
545
|
+
// tries `es-mx` and then `es` before falling through to the default. Matching
|
|
546
|
+
// was `===`, which made a POSIX `es_ES` card — what the Learnosity transform
|
|
547
|
+
// emits — unreachable for an `es-ES` request except through the final
|
|
548
|
+
// no-constraint rung, i.e. by accident.
|
|
542
549
|
const languageRungs = [];
|
|
550
|
+
const pushLookupRungs = (tag) => {
|
|
551
|
+
for (const step of languageTagLookupSequence(tag)) {
|
|
552
|
+
languageRungs.push((card) => normalizeLanguageTag(card.language) === step);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
543
555
|
if (language) {
|
|
544
|
-
|
|
556
|
+
pushLookupRungs(language);
|
|
545
557
|
}
|
|
546
558
|
if (useFallback) {
|
|
547
|
-
|
|
559
|
+
pushLookupRungs(this.defaultLanguage);
|
|
548
560
|
languageRungs.push(() => true);
|
|
549
561
|
}
|
|
550
562
|
for (const matchesLanguage of languageRungs) {
|
|
@@ -586,7 +598,11 @@ export class AccessibilityCatalogResolver {
|
|
|
586
598
|
// asking what alternates exist under-reported them.
|
|
587
599
|
const claimed = new Set();
|
|
588
600
|
const add = (card, source) => {
|
|
589
|
-
|
|
601
|
+
// The language part is normalized, so a POSIX `es_ES` card and a BCP-47
|
|
602
|
+
// `es-ES` card collapse to one entry here exactly as they now collapse on
|
|
603
|
+
// the resolution path. Keying on the raw string would report two
|
|
604
|
+
// alternates where resolution can only ever return one.
|
|
605
|
+
const key = `${card.catalog}|${normalizeLanguageTag(card.language)}|${catalogCardForm(card)}`;
|
|
590
606
|
if (claimed.has(key))
|
|
591
607
|
return;
|
|
592
608
|
claimed.add(key);
|