@pie-players/pie-assessment-toolkit 1.0.0-beta.2 → 1.0.0-beta.4
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 +15 -7
- package/custom-elements.json +19 -0
- package/dist/components/ItemScope.custom-element.js +1 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-4vq9nbxz.js +2 -0
- package/dist/components/chunks/{ItemToolBar-jpac75fa.js → ItemToolBar-f971t0cw.js} +1 -1
- package/dist/components/chunks/{ItemToolBar-341vf20n.js → ItemToolBar-n05xjkp8.js} +2 -2
- package/dist/components/chunks/{ItemToolBar-66hcwmfw.js → ItemToolBar-pxan157g.js} +2 -2
- package/dist/components/chunks/ItemToolBar-rgmvw9sm.js +40 -0
- package/dist/components/chunks/{ItemToolBar-kkj69ptr.js → ItemToolBar-xwjrpdmz.js} +1 -1
- package/dist/components/chunks/sre-engine-dq0w45fv.js +1 -0
- package/dist/policy/core/ToolPolicyEngine.d.ts +8 -0
- package/dist/policy/core/ToolPolicyEngine.js +4 -2
- package/dist/policy/core/compose-decision.d.ts +7 -1
- package/dist/policy/core/compose-decision.js +29 -0
- package/dist/policy/core/decision-types.d.ts +20 -0
- package/dist/policy/core/feature-decision.d.ts +8 -1
- package/dist/policy/core/feature-decision.js +9 -2
- package/dist/policy/core/provenance.d.ts +1 -1
- package/dist/policy/engine.d.ts +2 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +44 -2
- package/dist/policy/sources/PnpPolicySource.js +80 -11
- package/dist/runtime/core/engine-resolver.d.ts +17 -0
- package/dist/runtime/item-feature-decisions.d.ts +44 -0
- package/dist/runtime/item-feature-decisions.js +115 -0
- package/dist/services/TTSService.d.ts +1 -0
- package/dist/services/TTSService.js +10 -1
- package/dist/services/ToolkitCoordinator.d.ts +9 -0
- package/dist/services/ToolkitCoordinator.js +23 -0
- package/dist/services/interfaces.d.ts +10 -0
- package/dist/services/support-conflicts.d.ts +10 -0
- package/dist/services/support-conflicts.js +11 -0
- package/dist/services/tool-config-validation.js +8 -2
- package/dist/services/tool-icons.js +1 -0
- package/package.json +6 -6
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +0 -2
- package/dist/components/chunks/ItemToolBar-wbem8cna.js +0 -40
- package/dist/components/chunks/sre-engine-bj997w4b.js +0 -1
|
@@ -79,6 +79,11 @@ export interface ToolPolicyDecisionRequest {
|
|
|
79
79
|
* `restrictedTools` or the profile's `prohibitedSupports` withdraws instead.
|
|
80
80
|
* `details` is {@link OverrideBlockedDetails}.
|
|
81
81
|
*
|
|
82
|
+
* `tool-policy.supportConflict` fires for each id a support in effect withdraws
|
|
83
|
+
* that something would otherwise have served, as the toolkit's `screenReader`
|
|
84
|
+
* withdraws `textToSpeech`. It replaces `tool-policy.overrideBlocked` for a `true`
|
|
85
|
+
* override the conflict withdraws. `details` is {@link SupportConflictDetails}.
|
|
86
|
+
*
|
|
82
87
|
* The toolkit coordinator logs each diagnostic once per code, tool and item,
|
|
83
88
|
* and hands the same ones to its `onPolicyDiagnostic` listeners.
|
|
84
89
|
*/
|
|
@@ -122,6 +127,20 @@ export interface OverrideBlockedDetails {
|
|
|
122
127
|
/** The rule that withdrew the granted tool. */
|
|
123
128
|
rule: "item-restriction" | "pnp-prohibited";
|
|
124
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* What would have served an id a support conflict withdraws: the placement
|
|
132
|
+
* that put the tool on this toolbar, the rule that would have granted it, or,
|
|
133
|
+
* for a feature an item player applies, the player's attribute or the item's
|
|
134
|
+
* authored setting it displaced.
|
|
135
|
+
*/
|
|
136
|
+
export type SupportConflictServedBy = "placement" | "attribute" | "authored" | Extract<PnpPolicySourceRule, "test-admin-override" | "item-requirement" | "district-requirement" | "pnp-activate-at-init" | "pnp-support">;
|
|
137
|
+
/** Payload of a `tool-policy.supportConflict` diagnostic. */
|
|
138
|
+
export interface SupportConflictDetails {
|
|
139
|
+
/** The support in effect that withholds the id. */
|
|
140
|
+
support: string;
|
|
141
|
+
/** What would otherwise have served the withheld id. */
|
|
142
|
+
servedBy: SupportConflictServedBy;
|
|
143
|
+
}
|
|
125
144
|
/** Payload of a `tool-policy.itemSettingNotApplied` diagnostic. */
|
|
126
145
|
export interface ItemSettingNotAppliedDetails {
|
|
127
146
|
/** Canonical id of the item whose setting names the tool. */
|
|
@@ -138,6 +157,7 @@ export interface ToolPolicyDiagnosticDetails {
|
|
|
138
157
|
"tool-policy.unknownSupportId": UnknownSupportIdDetails;
|
|
139
158
|
"tool-policy.itemSettingNotApplied": ItemSettingNotAppliedDetails;
|
|
140
159
|
"tool-policy.overrideBlocked": OverrideBlockedDetails;
|
|
160
|
+
"tool-policy.supportConflict": SupportConflictDetails;
|
|
141
161
|
}
|
|
142
162
|
/** A policy diagnostic, its `details` typed by its `code`. */
|
|
143
163
|
export type ToolPolicyDiagnostic = {
|
|
@@ -77,9 +77,16 @@ export interface FeaturePolicyDecision<P = Record<string, unknown>> {
|
|
|
77
77
|
* later ones winning. Set on a denial too; a host denial leaves it out.
|
|
78
78
|
*/
|
|
79
79
|
parameters?: P;
|
|
80
|
+
/**
|
|
81
|
+
* The support in effect that withholds the feature, on a `support-conflict`
|
|
82
|
+
* verdict.
|
|
83
|
+
*/
|
|
84
|
+
withheldBy?: string;
|
|
80
85
|
/**
|
|
81
86
|
* Conflicts found while deciding: a `tool-policy.overrideBlocked` for a
|
|
82
|
-
* granting override the decision withdrew
|
|
87
|
+
* granting override the decision withdrew, and a
|
|
88
|
+
* `tool-policy.supportConflict` for a granting rule a support in effect
|
|
89
|
+
* displaced. Empty on a host denial, which
|
|
83
90
|
* consults no policy source. An id no tool is registered under describes the
|
|
84
91
|
* inputs, so it is reported on the engine inputs' `diagnostics`.
|
|
85
92
|
*/
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* "nothing was bound to ask against" are the same verdict for different
|
|
21
21
|
* reasons, and only the second is a defect.
|
|
22
22
|
*/
|
|
23
|
-
import { overrideBlockedDiagnostic } from "./compose-decision.js";
|
|
23
|
+
import { overrideBlockedDiagnostic, supportConflictDiagnostic, } from "./compose-decision.js";
|
|
24
24
|
/**
|
|
25
25
|
* Reason text for a denial that had no assessment to decide against.
|
|
26
26
|
*
|
|
@@ -79,6 +79,7 @@ export function interpretFeatureResult(featureId, result, context, parameters) {
|
|
|
79
79
|
// The source logs "no rule fired" as a skip at the last level so the trail
|
|
80
80
|
// stays ordered; the verdict names no rule for it.
|
|
81
81
|
const fired = decision !== undefined && decision.action !== "skip";
|
|
82
|
+
const conflict = result.supportConflicts.get(featureId);
|
|
82
83
|
return {
|
|
83
84
|
featureId,
|
|
84
85
|
granted,
|
|
@@ -95,7 +96,13 @@ export function interpretFeatureResult(featureId, result, context, parameters) {
|
|
|
95
96
|
: unboundAssessmentReason(featureId),
|
|
96
97
|
required: Boolean(flags?.required),
|
|
97
98
|
parameters,
|
|
99
|
+
...(fired && decision.rule === "support-conflict" && conflict
|
|
100
|
+
? { withheldBy: conflict.support }
|
|
101
|
+
: {}),
|
|
98
102
|
assessmentBound: context.assessmentBound,
|
|
99
|
-
diagnostics:
|
|
103
|
+
diagnostics: [
|
|
104
|
+
...Array.from(result.blockedOverrides, ([toolId, rule]) => overrideBlockedDiagnostic(toolId, rule)),
|
|
105
|
+
...Array.from(result.supportConflicts).flatMap(([toolId, { support, grant }]) => grant ? [supportConflictDiagnostic(toolId, support, grant)] : []),
|
|
106
|
+
],
|
|
100
107
|
};
|
|
101
108
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* provider veto, PNP/profile gates, and custom policy sources.
|
|
9
9
|
*/
|
|
10
10
|
/** The PNP/profile precedence rules, highest first. */
|
|
11
|
-
export type PnpPolicySourceRule = "district-block" | "test-admin-override" | "item-restriction" | "pnp-prohibited" | "item-requirement" | "district-requirement" | "pnp-support" | "pnp-activate-at-init";
|
|
11
|
+
export type PnpPolicySourceRule = "district-block" | "test-admin-override" | "item-restriction" | "pnp-prohibited" | "support-conflict" | "item-requirement" | "district-requirement" | "pnp-support" | "pnp-activate-at-init";
|
|
12
12
|
/**
|
|
13
13
|
* Rule vocabulary for tool policy decisions. A decision's `rule` is the one
|
|
14
14
|
* attribution channel: it names the step that enabled, blocked or advised on a
|
package/dist/policy/engine.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { ToolPolicyEngine, type PnpEnforcementMode, type ResolvedEngineInputs, type ToolPolicyChangeEvent, type ToolPolicyChangeListener, type ToolPolicyEngineArgs, type ToolPolicyEngineInputs, } from "./core/ToolPolicyEngine.js";
|
|
9
9
|
export { isHostDeniedFeature, type FeaturePolicyDecision, type FeaturePolicyRule, } from "./core/feature-decision.js";
|
|
10
|
-
export type { ItemSettingNotAppliedDetails, OverrideBlockedDetails, PlacementMissingDetails, RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyDiagnosticDetails, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, UnknownSupportIdDetails, } from "./core/decision-types.js";
|
|
10
|
+
export type { ItemSettingNotAppliedDetails, OverrideBlockedDetails, PlacementMissingDetails, RequiredToolBlockedDetails, SupportConflictDetails, SupportConflictServedBy, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyDiagnosticDetails, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, UnknownSupportIdDetails, } from "./core/decision-types.js";
|
|
11
11
|
export type { PolicySource, PolicySourceDecisionContext, PolicySourceProvenanceEntry, PolicySourceResult, } from "./core/PolicySource.js";
|
|
12
|
+
export type { SupportConflict } from "./sources/PnpPolicySource.js";
|
|
12
13
|
export type { PnpPolicySourceRule, ToolPolicyDecisionRule, ToolPolicyFeatureTrail, ToolPolicyProvenance, ToolPolicyResolutionDecision, ToolPolicySourceType, } from "./core/provenance.js";
|
|
@@ -10,7 +10,19 @@
|
|
|
10
10
|
import type { AssessmentEntity, ItemSettings } from "@pie-players/pie-players-shared/types";
|
|
11
11
|
import type { ToolRegistry } from "../../services/ToolRegistry.js";
|
|
12
12
|
import type { PnpPolicySourceRule, ToolPolicyResolutionDecision, ToolPolicySourceType } from "../core/provenance.js";
|
|
13
|
-
import type { OverrideBlockedDetails } from "../core/decision-types.js";
|
|
13
|
+
import type { OverrideBlockedDetails, SupportConflictServedBy } from "../core/decision-types.js";
|
|
14
|
+
/**
|
|
15
|
+
* A support whose being in effect withholds other ids: they resolve blocked
|
|
16
|
+
* under `support-conflict` at precedence 4 while `support` resolves enabled.
|
|
17
|
+
* Every id a conflict names is recognized without a tool registration, so none
|
|
18
|
+
* raises `tool-policy.unknownSupportId`.
|
|
19
|
+
*/
|
|
20
|
+
export interface SupportConflict {
|
|
21
|
+
/** The support id that withholds. */
|
|
22
|
+
support: string;
|
|
23
|
+
/** The ids withheld while `support` is in effect. */
|
|
24
|
+
withholds: readonly string[];
|
|
25
|
+
}
|
|
14
26
|
/** Per-tool flags PNP/profile policy may attach to a surviving entry. */
|
|
15
27
|
export interface PnpPolicyToolFlags {
|
|
16
28
|
/** PNP/profile policy mandates this tool (item or district `requiredTools`). */
|
|
@@ -82,6 +94,14 @@ export interface PnpPolicyResult {
|
|
|
82
94
|
* engine turns each into a `tool-policy.overrideBlocked` diagnostic.
|
|
83
95
|
*/
|
|
84
96
|
blockedOverrides: Map<string, OverrideBlockedDetails["rule"]>;
|
|
97
|
+
/**
|
|
98
|
+
* Ids a support in effect withdrew under `support-conflict`, each with that
|
|
99
|
+
* support and the rule that would otherwise have granted the id, absent when
|
|
100
|
+
* none would have. The engine turns each into a
|
|
101
|
+
* `tool-policy.supportConflict` diagnostic when something would have served
|
|
102
|
+
* the id.
|
|
103
|
+
*/
|
|
104
|
+
supportConflicts: Map<string, SupportConflictResolution>;
|
|
85
105
|
/** Configuration sources the engine should attach to its provenance. */
|
|
86
106
|
sources: {
|
|
87
107
|
assessment?: {
|
|
@@ -101,10 +121,21 @@ export interface PnpPolicyResult {
|
|
|
101
121
|
};
|
|
102
122
|
};
|
|
103
123
|
}
|
|
124
|
+
/** A rule a support conflict can displace: one of levels 5 to 8. */
|
|
125
|
+
export type SupportConflictGrant = Exclude<SupportConflictServedBy, "placement" | "attribute" | "authored">;
|
|
126
|
+
/** One `support-conflict` withdrawal. */
|
|
127
|
+
export interface SupportConflictResolution {
|
|
128
|
+
support: string;
|
|
129
|
+
grant?: SupportConflictGrant;
|
|
130
|
+
}
|
|
104
131
|
export declare class PnpPolicySource {
|
|
105
132
|
readonly id = "pnp";
|
|
106
133
|
private readonly toolRegistry;
|
|
107
|
-
|
|
134
|
+
private readonly supportConflicts;
|
|
135
|
+
/** Withheld id to the supports that withhold it, in declaration order. */
|
|
136
|
+
private readonly withheldBy;
|
|
137
|
+
private readonly registrylessIds;
|
|
138
|
+
constructor(toolRegistry: ToolRegistry, supportConflicts?: readonly SupportConflict[]);
|
|
108
139
|
apply(args: PnpPolicyApplyArgs): PnpPolicyResult;
|
|
109
140
|
/**
|
|
110
141
|
* Evaluate exactly one PNP support id through the same eight-level
|
|
@@ -126,6 +157,17 @@ export declare class PnpPolicySource {
|
|
|
126
157
|
* configuration-source attribution) from the bound policy inputs.
|
|
127
158
|
*/
|
|
128
159
|
private prepare;
|
|
160
|
+
/**
|
|
161
|
+
* Whether `support` resolves enabled, conflicts aside: a support another
|
|
162
|
+
* conflict withholds still withholds, and two conflicts naming each other
|
|
163
|
+
* cannot recurse.
|
|
164
|
+
*/
|
|
165
|
+
private supportInEffect;
|
|
166
|
+
/**
|
|
167
|
+
* `withConflict: false` resolves the id as if no support conflict applied,
|
|
168
|
+
* which names the grant a conflict displaces without a second copy of
|
|
169
|
+
* levels 5 to 8.
|
|
170
|
+
*/
|
|
129
171
|
private resolveSupport;
|
|
130
172
|
/**
|
|
131
173
|
* Ids the assessment or any of `items` names that no tool is registered
|
|
@@ -10,8 +10,22 @@
|
|
|
10
10
|
export class PnpPolicySource {
|
|
11
11
|
id = "pnp";
|
|
12
12
|
toolRegistry;
|
|
13
|
-
|
|
13
|
+
supportConflicts;
|
|
14
|
+
/** Withheld id to the supports that withhold it, in declaration order. */
|
|
15
|
+
withheldBy = new Map();
|
|
16
|
+
registrylessIds = new Set();
|
|
17
|
+
constructor(toolRegistry, supportConflicts = []) {
|
|
14
18
|
this.toolRegistry = toolRegistry;
|
|
19
|
+
this.supportConflicts = supportConflicts;
|
|
20
|
+
for (const { support, withholds } of supportConflicts) {
|
|
21
|
+
this.registrylessIds.add(support);
|
|
22
|
+
for (const id of withholds) {
|
|
23
|
+
this.registrylessIds.add(id);
|
|
24
|
+
const supports = this.withheldBy.get(id) ?? [];
|
|
25
|
+
supports.push(support);
|
|
26
|
+
this.withheldBy.set(id, supports);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
}
|
|
16
30
|
apply(args) {
|
|
17
31
|
const { ctx, result } = this.prepare(args);
|
|
@@ -27,6 +41,12 @@ export class PnpPolicySource {
|
|
|
27
41
|
}
|
|
28
42
|
itemSettings?.requiredTools?.forEach((s) => allSupports.add(s));
|
|
29
43
|
itemSettings?.restrictedTools?.forEach((s) => allSupports.add(s));
|
|
44
|
+
// A conflict withdraws a placed tool that no policy input names.
|
|
45
|
+
for (const { support, withholds } of this.supportConflicts) {
|
|
46
|
+
if (this.supportInEffect(support, ctx)) {
|
|
47
|
+
withholds.forEach((s) => allSupports.add(s));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
30
50
|
for (const supportId of allSupports) {
|
|
31
51
|
this.resolveSupport(supportId, ctx, result);
|
|
32
52
|
}
|
|
@@ -60,14 +80,7 @@ export class PnpPolicySource {
|
|
|
60
80
|
const pnp = assessment?.personalNeedsProfile;
|
|
61
81
|
const settings = assessment?.settings;
|
|
62
82
|
const itemSettings = item?.settings;
|
|
63
|
-
const result =
|
|
64
|
-
blockedToolIds: new Set(),
|
|
65
|
-
mandates: new Map(),
|
|
66
|
-
perToolFlags: new Map(),
|
|
67
|
-
decisions: [],
|
|
68
|
-
blockedOverrides: new Map(),
|
|
69
|
-
sources: {},
|
|
70
|
-
};
|
|
83
|
+
const result = emptyResult();
|
|
71
84
|
if (settings?.districtPolicy || settings?.testAdministration) {
|
|
72
85
|
result.sources.assessment = {
|
|
73
86
|
id: assessment?.id || "unknown",
|
|
@@ -97,7 +110,22 @@ export class PnpPolicySource {
|
|
|
97
110
|
};
|
|
98
111
|
return { ctx, result };
|
|
99
112
|
}
|
|
100
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Whether `support` resolves enabled, conflicts aside: a support another
|
|
115
|
+
* conflict withholds still withholds, and two conflicts naming each other
|
|
116
|
+
* cannot recurse.
|
|
117
|
+
*/
|
|
118
|
+
supportInEffect(support, ctx) {
|
|
119
|
+
const scratch = emptyResult();
|
|
120
|
+
this.resolveSupport(support, ctx, scratch, false);
|
|
121
|
+
return scratch.decisions[0]?.action === "enable";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `withConflict: false` resolves the id as if no support conflict applied,
|
|
125
|
+
* which names the grant a conflict displaces without a second copy of
|
|
126
|
+
* levels 5 to 8.
|
|
127
|
+
*/
|
|
128
|
+
resolveSupport(supportId, ctx, out, withConflict = true) {
|
|
101
129
|
// 1. District block (absolute veto)
|
|
102
130
|
if (ctx.districtPolicy?.blockedTools?.includes(supportId)) {
|
|
103
131
|
out.blockedToolIds.add(supportId);
|
|
@@ -163,6 +191,35 @@ export class PnpPolicySource {
|
|
|
163
191
|
});
|
|
164
192
|
return;
|
|
165
193
|
}
|
|
194
|
+
// 4. Support conflict. Ranks with the profile prohibition, below the
|
|
195
|
+
// blocks a person recorded.
|
|
196
|
+
const withholding = withConflict
|
|
197
|
+
? this.withheldBy
|
|
198
|
+
.get(supportId)
|
|
199
|
+
?.find((support) => this.supportInEffect(support, ctx))
|
|
200
|
+
: undefined;
|
|
201
|
+
if (withholding) {
|
|
202
|
+
const displaced = emptyResult();
|
|
203
|
+
this.resolveSupport(supportId, ctx, displaced, false);
|
|
204
|
+
const grant = displaced.decisions[0];
|
|
205
|
+
out.blockedToolIds.add(supportId);
|
|
206
|
+
out.supportConflicts.set(supportId, {
|
|
207
|
+
support: withholding,
|
|
208
|
+
...(grant?.action === "enable"
|
|
209
|
+
? { grant: grant.rule }
|
|
210
|
+
: {}),
|
|
211
|
+
});
|
|
212
|
+
out.decisions.push({
|
|
213
|
+
precedence: 4,
|
|
214
|
+
rule: "support-conflict",
|
|
215
|
+
featureId: supportId,
|
|
216
|
+
action: "block",
|
|
217
|
+
sourceType: "student",
|
|
218
|
+
reason: `"${supportId}" is withheld while the student's "${withholding}" support is in effect`,
|
|
219
|
+
value: { support: withholding },
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
166
223
|
const activateAtInit = ctx.pnp?.activateAtInit?.includes(supportId) === true;
|
|
167
224
|
const mandate = ctx.itemSettings?.requiredTools?.includes(supportId)
|
|
168
225
|
? "item-requirement"
|
|
@@ -311,8 +368,9 @@ export class PnpPolicySource {
|
|
|
311
368
|
];
|
|
312
369
|
for (const [rule, ids] of named) {
|
|
313
370
|
for (const id of ids ?? []) {
|
|
314
|
-
if (this.toolRegistry.has(id))
|
|
371
|
+
if (this.toolRegistry.has(id) || this.registrylessIds.has(id)) {
|
|
315
372
|
continue;
|
|
373
|
+
}
|
|
316
374
|
const rules = unmapped.get(id) ?? [];
|
|
317
375
|
if (!rules.includes(rule))
|
|
318
376
|
rules.push(rule);
|
|
@@ -322,3 +380,14 @@ export class PnpPolicySource {
|
|
|
322
380
|
return unmapped;
|
|
323
381
|
}
|
|
324
382
|
}
|
|
383
|
+
function emptyResult() {
|
|
384
|
+
return {
|
|
385
|
+
blockedToolIds: new Set(),
|
|
386
|
+
mandates: new Map(),
|
|
387
|
+
perToolFlags: new Map(),
|
|
388
|
+
decisions: [],
|
|
389
|
+
blockedOverrides: new Map(),
|
|
390
|
+
supportConflicts: new Map(),
|
|
391
|
+
sources: {},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* `toolConfigStrictness` beside `runtime`.
|
|
23
23
|
*/
|
|
24
24
|
import type { LoaderConfig } from "@pie-players/pie-players-shared/loader-config";
|
|
25
|
+
import type { I18nProvider } from "@pie-players/pie-players-shared/i18n";
|
|
25
26
|
import type { LoadingCompleteDetail, StageChangeDetail } from "@pie-players/pie-players-shared/pie";
|
|
26
27
|
import type { FrameworkErrorModel } from "../../services/framework-error.js";
|
|
27
28
|
import type { ToolConfigStrictness } from "../../services/tool-config-validation.js";
|
|
@@ -71,6 +72,14 @@ export type RuntimeConfig = {
|
|
|
71
72
|
* Unset reads as `en-US` wherever the markup names no language.
|
|
72
73
|
*/
|
|
73
74
|
contentLanguage?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The interface-locale provider the toolkit publishes on its runtime
|
|
77
|
+
* context, in place of the one it would construct: the catalogs it can
|
|
78
|
+
* serve, their loaders, and any host messages. `locale` on the element
|
|
79
|
+
* still names the locale; a provider given with none set keeps the locale
|
|
80
|
+
* its owner gave it.
|
|
81
|
+
*/
|
|
82
|
+
i18n?: I18nProvider;
|
|
74
83
|
onFrameworkError?: FrameworkErrorHandler;
|
|
75
84
|
/** Called with each `pie-stage-change` detail. */
|
|
76
85
|
onStageChange?: StageChangeHandler;
|
|
@@ -131,6 +140,14 @@ export declare function resolveRuntime(args: {
|
|
|
131
140
|
* Unset reads as `en-US` wherever the markup names no language.
|
|
132
141
|
*/
|
|
133
142
|
contentLanguage?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The interface-locale provider the toolkit publishes on its runtime
|
|
145
|
+
* context, in place of the one it would construct: the catalogs it can
|
|
146
|
+
* serve, their loaders, and any host messages. `locale` on the element
|
|
147
|
+
* still names the locale; a provider given with none set keeps the locale
|
|
148
|
+
* its owner gave it.
|
|
149
|
+
*/
|
|
150
|
+
i18n?: I18nProvider;
|
|
134
151
|
onFrameworkError?: FrameworkErrorHandler;
|
|
135
152
|
/** Called with each `pie-stage-change` detail. */
|
|
136
153
|
onStageChange?: StageChangeHandler;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ItemFeatureDecisions } from "@pie-players/pie-context";
|
|
2
|
+
import type { ToolScope } from "../policy/core/decision-types.js";
|
|
3
|
+
import type { ToolkitCoordinatorApi } from "../services/interfaces.js";
|
|
4
|
+
/** The feature ids an item player applies to its item's models. */
|
|
5
|
+
export declare const ITEM_PLAYER_FEATURE_IDS: readonly ["autoplayAudio"];
|
|
6
|
+
type DecidingCoordinator = Pick<ToolkitCoordinatorApi, "decideFeaturePolicy" | "onPolicyChange" | "reportFeatureDecisionApplied">;
|
|
7
|
+
/** The item a scope asks feature policy for. */
|
|
8
|
+
export interface ItemFeatureScopeIdentity {
|
|
9
|
+
itemId: string;
|
|
10
|
+
canonicalItemId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The scope an item's feature decisions are asked with, which brings in the
|
|
14
|
+
* item's registered settings; the same scope an item's content surfaces ask
|
|
15
|
+
* with.
|
|
16
|
+
*/
|
|
17
|
+
export declare function itemFeatureScope(identity: ItemFeatureScopeIdentity): ToolScope;
|
|
18
|
+
/**
|
|
19
|
+
* The decisions an item player applies, leaving out each id no rule names. No
|
|
20
|
+
* coordinator, or one predating feature decisions, decides nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveItemFeatureDecisions(coordinator: Pick<ToolkitCoordinatorApi, "decideFeaturePolicy"> | null, scope: ToolScope | null): ItemFeatureDecisions;
|
|
23
|
+
export interface ItemFeatureDecisionsProvider {
|
|
24
|
+
/**
|
|
25
|
+
* The coordinator the scope's toolkit answered with; `null` until one has.
|
|
26
|
+
* The provider follows its policy changes, an item's settings registering
|
|
27
|
+
* among them.
|
|
28
|
+
*/
|
|
29
|
+
setCoordinator(coordinator: DecidingCoordinator | null): void;
|
|
30
|
+
/** The scope's item; `null` while it has no item id. */
|
|
31
|
+
setItem(identity: ItemFeatureScopeIdentity | null): void;
|
|
32
|
+
disconnect(): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Answers `itemFeatureDecisionsContext` for the item player under `host`, and
|
|
36
|
+
* relays the player's `feature-decision-applied` to the coordinator.
|
|
37
|
+
*
|
|
38
|
+
* It answers with no decision until both a coordinator and an item are set, so
|
|
39
|
+
* an item player under a scope no toolkit has answered keeps its own setting.
|
|
40
|
+
* A value is republished only when a decision changed, since every new value
|
|
41
|
+
* remounts the item.
|
|
42
|
+
*/
|
|
43
|
+
export declare function provideItemFeatureDecisions(host: HTMLElement): ItemFeatureDecisionsProvider;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ContextProvider, FEATURE_DECISION_APPLIED_EVENT, itemFeatureDecisionsContext, } from "@pie-players/pie-context";
|
|
2
|
+
/** The feature ids an item player applies to its item's models. */
|
|
3
|
+
export const ITEM_PLAYER_FEATURE_IDS = ["autoplayAudio"];
|
|
4
|
+
const NO_DECISIONS = Object.freeze({
|
|
5
|
+
features: Object.freeze({}),
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* The scope an item's feature decisions are asked with, which brings in the
|
|
9
|
+
* item's registered settings; the same scope an item's content surfaces ask
|
|
10
|
+
* with.
|
|
11
|
+
*/
|
|
12
|
+
export function itemFeatureScope(identity) {
|
|
13
|
+
const canonicalItemId = identity.canonicalItemId || identity.itemId;
|
|
14
|
+
return {
|
|
15
|
+
level: "item",
|
|
16
|
+
scopeId: canonicalItemId,
|
|
17
|
+
itemId: identity.itemId,
|
|
18
|
+
canonicalItemId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The decisions an item player applies, leaving out each id no rule names. No
|
|
23
|
+
* coordinator, or one predating feature decisions, decides nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveItemFeatureDecisions(coordinator, scope) {
|
|
26
|
+
if (!scope || typeof coordinator?.decideFeaturePolicy !== "function") {
|
|
27
|
+
return NO_DECISIONS;
|
|
28
|
+
}
|
|
29
|
+
const features = {};
|
|
30
|
+
for (const featureId of ITEM_PLAYER_FEATURE_IDS) {
|
|
31
|
+
const decision = coordinator.decideFeaturePolicy(featureId, scope);
|
|
32
|
+
if (decision.rule === "none")
|
|
33
|
+
continue;
|
|
34
|
+
features[featureId] = { granted: decision.granted, rule: decision.rule };
|
|
35
|
+
}
|
|
36
|
+
return { features };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Answers `itemFeatureDecisionsContext` for the item player under `host`, and
|
|
40
|
+
* relays the player's `feature-decision-applied` to the coordinator.
|
|
41
|
+
*
|
|
42
|
+
* It answers with no decision until both a coordinator and an item are set, so
|
|
43
|
+
* an item player under a scope no toolkit has answered keeps its own setting.
|
|
44
|
+
* A value is republished only when a decision changed, since every new value
|
|
45
|
+
* remounts the item.
|
|
46
|
+
*/
|
|
47
|
+
export function provideItemFeatureDecisions(host) {
|
|
48
|
+
const provider = new ContextProvider(host, {
|
|
49
|
+
context: itemFeatureDecisionsContext,
|
|
50
|
+
initialValue: NO_DECISIONS,
|
|
51
|
+
});
|
|
52
|
+
provider.connect();
|
|
53
|
+
let signature = JSON.stringify(NO_DECISIONS.features);
|
|
54
|
+
let coordinator = null;
|
|
55
|
+
let scope = null;
|
|
56
|
+
let stopFollowingPolicy = null;
|
|
57
|
+
const refresh = () => {
|
|
58
|
+
const next = resolveItemFeatureDecisions(coordinator, scope);
|
|
59
|
+
const nextSignature = JSON.stringify(next.features);
|
|
60
|
+
if (nextSignature === signature)
|
|
61
|
+
return;
|
|
62
|
+
signature = nextSignature;
|
|
63
|
+
provider.setValue(next);
|
|
64
|
+
};
|
|
65
|
+
const stopFollowing = () => {
|
|
66
|
+
const stop = stopFollowingPolicy;
|
|
67
|
+
stopFollowingPolicy = null;
|
|
68
|
+
try {
|
|
69
|
+
stop?.();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// The coordinator may already have been disposed.
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const onApplied = (event) => {
|
|
76
|
+
const detail = event.detail;
|
|
77
|
+
if (!detail || !scope)
|
|
78
|
+
return;
|
|
79
|
+
coordinator?.reportFeatureDecisionApplied?.(detail, scope);
|
|
80
|
+
};
|
|
81
|
+
host.addEventListener(FEATURE_DECISION_APPLIED_EVENT, onApplied);
|
|
82
|
+
return {
|
|
83
|
+
setCoordinator(next) {
|
|
84
|
+
if (next === coordinator)
|
|
85
|
+
return;
|
|
86
|
+
stopFollowing();
|
|
87
|
+
coordinator = next;
|
|
88
|
+
if (next && typeof next.onPolicyChange === "function") {
|
|
89
|
+
stopFollowingPolicy = next.onPolicyChange((event) => {
|
|
90
|
+
if (event.reason === "disposed") {
|
|
91
|
+
stopFollowing();
|
|
92
|
+
coordinator = null;
|
|
93
|
+
}
|
|
94
|
+
refresh();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
refresh();
|
|
98
|
+
},
|
|
99
|
+
setItem(identity) {
|
|
100
|
+
const next = identity?.itemId ? itemFeatureScope(identity) : null;
|
|
101
|
+
if (next?.scopeId === scope?.scopeId && next?.itemId === scope?.itemId) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
scope = next;
|
|
105
|
+
refresh();
|
|
106
|
+
},
|
|
107
|
+
disconnect() {
|
|
108
|
+
host.removeEventListener(FEATURE_DECISION_APPLIED_EVENT, onApplied);
|
|
109
|
+
stopFollowing();
|
|
110
|
+
coordinator = null;
|
|
111
|
+
scope = null;
|
|
112
|
+
provider.disconnect();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -172,6 +172,9 @@ export class TTSService {
|
|
|
172
172
|
// Bumped by every new run and by stop, restart and dispose; an async step
|
|
173
173
|
// that resumes under an older id has been superseded and returns.
|
|
174
174
|
speakRunId = 0;
|
|
175
|
+
// The run a seek or speed change restarted. While it loads, a seek counts
|
|
176
|
+
// from its target, so repeated presses add up.
|
|
177
|
+
restartRunId = null;
|
|
175
178
|
runOwner = null;
|
|
176
179
|
// currentBoundaryOffset is the spoken-text offset the run has reached, which
|
|
177
180
|
// seek and word boundaries count from; activeWordBoundaryOffset is where the
|
|
@@ -2271,6 +2274,7 @@ export class TTSService {
|
|
|
2271
2274
|
this.provider.onWordBoundary = undefined;
|
|
2272
2275
|
this.provider.stop();
|
|
2273
2276
|
const runId = ++this.speakRunId;
|
|
2277
|
+
this.restartRunId = runId;
|
|
2274
2278
|
const restartSegments = this.seekSegments.slice(safeTargetIndex);
|
|
2275
2279
|
this.highlightCoordinator?.clearTTS();
|
|
2276
2280
|
this.activeSentenceStartOffset = null;
|
|
@@ -2392,8 +2396,13 @@ export class TTSService {
|
|
|
2392
2396
|
async seekBy(units) {
|
|
2393
2397
|
if (!this.provider || !this.currentText)
|
|
2394
2398
|
return;
|
|
2399
|
+
// A restart that is still loading has set the cursor to its target. The
|
|
2400
|
+
// first load of a read has not, so a seek waits for it to play.
|
|
2401
|
+
const restartLoading = this.state === PlaybackState.LOADING &&
|
|
2402
|
+
this.restartRunId === this.speakRunId;
|
|
2395
2403
|
if (this.state !== PlaybackState.PLAYING &&
|
|
2396
|
-
this.state !== PlaybackState.PAUSED
|
|
2404
|
+
this.state !== PlaybackState.PAUSED &&
|
|
2405
|
+
!restartLoading) {
|
|
2397
2406
|
return;
|
|
2398
2407
|
}
|
|
2399
2408
|
if (this.hasExplicitBreakSemantics(this.currentText))
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - config-change application and TTS reconfiguration, last.
|
|
23
23
|
*/
|
|
24
24
|
import type { AccessibilityCatalog, AssessmentEntity, ItemSettings, ToolParametersFor } from "@pie-players/pie-players-shared/types";
|
|
25
|
+
import type { FeatureDecisionAppliedDetail } from "@pie-players/pie-context";
|
|
25
26
|
import { type CanonicalToolsConfig, type ToolPlacementConfig, type ToolPolicyConfig, type TextToSpeechToolProviderConfig, type ToolProviderConfig, type ToolProvidersConfig, type ToolsConfigInput } from "./tools-config-normalizer.js";
|
|
26
27
|
import { type ToolConfigStrictness } from "./tool-config-validation.js";
|
|
27
28
|
import { AccessibilityCatalogResolver } from "./AccessibilityCatalogResolver.js";
|
|
@@ -789,6 +790,14 @@ export declare class ToolkitCoordinator {
|
|
|
789
790
|
* registered settings ({@link registerItemSettings}).
|
|
790
791
|
*/
|
|
791
792
|
decideFeaturePolicy<K extends string>(featureId: K, scope?: ToolScope): FeaturePolicyDecision<ToolParametersFor<K>>;
|
|
793
|
+
/**
|
|
794
|
+
* Hear that an item player applied a feature decision to its item's models.
|
|
795
|
+
* A `support-conflict` hold that displaced the player's attribute or the
|
|
796
|
+
* item's authored setting raises `tool-policy.supportConflict`, as a
|
|
797
|
+
* conflict that displaced a granting rule already did when it was decided.
|
|
798
|
+
* Every other block was recorded by a person and raises nothing.
|
|
799
|
+
*/
|
|
800
|
+
reportFeatureDecisionApplied(detail: FeatureDecisionAppliedDetail, scope: ToolScope): void;
|
|
792
801
|
/**
|
|
793
802
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
794
803
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
@@ -37,7 +37,9 @@ import { ToolProviderRegistry } from "./tool-providers/index.js";
|
|
|
37
37
|
import { ToolRegistry } from "./ToolRegistry.js";
|
|
38
38
|
import { ToolRequestRegistry } from "./tool-request.js";
|
|
39
39
|
import { ToolPolicyEngine, } from "../policy/engine.js";
|
|
40
|
+
import { supportConflictDiagnostic } from "../policy/core/compose-decision.js";
|
|
40
41
|
import { resolveSectionSessionAssignment } from "./section-session-assignment.js";
|
|
42
|
+
import { TOOLKIT_SUPPORT_CONFLICTS } from "./support-conflicts.js";
|
|
41
43
|
class ToolkitCoordinatorDisposedError extends Error {
|
|
42
44
|
constructor() {
|
|
43
45
|
super("ToolkitCoordinator has been disposed.");
|
|
@@ -367,6 +369,7 @@ export class ToolkitCoordinator {
|
|
|
367
369
|
toolRegistry: this.toolRegistry,
|
|
368
370
|
contextId: `toolkit-coordinator:${this.assessmentId}`,
|
|
369
371
|
assessmentOptional: this.config.assessmentOptional === true,
|
|
372
|
+
supportConflicts: TOOLKIT_SUPPORT_CONFLICTS,
|
|
370
373
|
inputs: {
|
|
371
374
|
tools: this.config.tools,
|
|
372
375
|
assessment: null,
|
|
@@ -2157,6 +2160,26 @@ export class ToolkitCoordinator {
|
|
|
2157
2160
|
}
|
|
2158
2161
|
return decision;
|
|
2159
2162
|
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Hear that an item player applied a feature decision to its item's models.
|
|
2165
|
+
* A `support-conflict` hold that displaced the player's attribute or the
|
|
2166
|
+
* item's authored setting raises `tool-policy.supportConflict`, as a
|
|
2167
|
+
* conflict that displaced a granting rule already did when it was decided.
|
|
2168
|
+
* Every other block was recorded by a person and raises nothing.
|
|
2169
|
+
*/
|
|
2170
|
+
reportFeatureDecisionApplied(detail, scope) {
|
|
2171
|
+
if (detail.granted ||
|
|
2172
|
+
detail.rule !== "support-conflict" ||
|
|
2173
|
+
(detail.overrode !== "attribute" && detail.overrode !== "authored")) {
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const decision = this.policyEngine.decideFeature(detail.featureId, scope);
|
|
2177
|
+
if (decision.rule !== "support-conflict" || !decision.withheldBy)
|
|
2178
|
+
return;
|
|
2179
|
+
this.warnPolicyDiagnostics([
|
|
2180
|
+
supportConflictDiagnostic(detail.featureId, decision.withheldBy, detail.overrode),
|
|
2181
|
+
]);
|
|
2182
|
+
}
|
|
2160
2183
|
/**
|
|
2161
2184
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
2162
2185
|
* coordinator's bound inputs change (`updateToolConfig`,
|
|
@@ -26,6 +26,7 @@ import type { ToolProviderApi } from "./tool-providers/ToolProviderApi.js";
|
|
|
26
26
|
import type { ToolProviderRegistry } from "./tool-providers/ToolProviderRegistry.js";
|
|
27
27
|
import type { FeaturePolicyDecision, PolicySource, PnpEnforcementMode, ResolvedEngineInputs, ToolPolicyChangeListener, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolScope } from "../policy/engine.js";
|
|
28
28
|
import type { AssessmentEntity, ItemSettings, ToolParametersFor } from "@pie-players/pie-players-shared/types";
|
|
29
|
+
import type { FeatureDecisionAppliedDetail } from "@pie-players/pie-context";
|
|
29
30
|
import type { TTSConfig } from "@pie-players/pie-tts";
|
|
30
31
|
import type { ResolvedToolContext, ToolContextResolverContext, ToolRegistry } from "./ToolRegistry.js";
|
|
31
32
|
import type { ToolOpenRequest, ToolRequestTarget } from "./tool-request.js";
|
|
@@ -615,6 +616,15 @@ export interface ToolkitCoordinatorApi {
|
|
|
615
616
|
* assignable; call sites must feature-detect.
|
|
616
617
|
*/
|
|
617
618
|
decideFeaturePolicy?<K extends string>(featureId: K, scope?: ToolScope): FeaturePolicyDecision<ToolParametersFor<K>>;
|
|
619
|
+
/**
|
|
620
|
+
* Hear that an item player applied a feature decision to its item's models,
|
|
621
|
+
* as `<pie-item-scope>` relays its `feature-decision-applied`. A
|
|
622
|
+
* `support-conflict` hold that displaced the player's attribute or the
|
|
623
|
+
* item's authored setting raises `tool-policy.supportConflict`.
|
|
624
|
+
*
|
|
625
|
+
* Optional, like {@link decideFeaturePolicy}; call sites must feature-detect.
|
|
626
|
+
*/
|
|
627
|
+
reportFeatureDecisionApplied?(detail: FeatureDecisionAppliedDetail, scope: ToolScope): void;
|
|
618
628
|
/**
|
|
619
629
|
* Subscribe to policy-engine change events. Fires whenever the
|
|
620
630
|
* coordinator's bound policy inputs change (`updateToolConfig`,
|