@pie-players/pie-assessment-toolkit 0.3.66 → 0.3.67
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/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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/tools/content-capability-resolution.d.ts +106 -0
- package/dist/tools/content-capability-resolution.js +136 -0
- package/dist/tools/internal.d.ts +2 -0
- package/dist/tools/internal.js +1 -0
- package/package.json +13 -9
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type { ToolConfigDiagnostic, ToolConfigDiagnosticSeverity, ToolConfigStri
|
|
|
53
53
|
export type { ToolbarButtonItem, ToolbarItem, ToolbarItemBase, ToolbarLinkItem, } from "./services/toolbar-items.js";
|
|
54
54
|
export { isExternalIconUrl, isInlineSvgIcon, isToolbarLinkItem, isValidToolbarItemShape, } from "./services/toolbar-items.js";
|
|
55
55
|
export { normalizeToolsConfig, normalizeToolAlias, normalizeToolList, parseToolList, } from "./services/tools-config-normalizer.js";
|
|
56
|
+
export { isHostDeniedFeature } from "./policy/core/feature-decision.js";
|
|
56
57
|
export { frameworkErrorFromToolConfigValidation, normalizeAndValidateToolsConfig, } from "./services/tool-config-validation.js";
|
|
57
58
|
export type { ParsedToolInstanceId, ToolScopeLevel, } from "./services/tool-instance-id.js";
|
|
58
59
|
export { createScopedToolId, parseScopedToolId, toOverlayToolId, } from "./services/tool-instance-id.js";
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export { ToolkitCoordinator } from "./services/ToolkitCoordinator.js";
|
|
|
42
42
|
export { formatFrameworkErrorForConsole, frameworkErrorFromToolConfigDiagnostics, frameworkErrorFromUnknown, toFrameworkErrorModel, } from "./services/framework-error.js";
|
|
43
43
|
export { isExternalIconUrl, isInlineSvgIcon, isToolbarLinkItem, isValidToolbarItemShape, } from "./services/toolbar-items.js";
|
|
44
44
|
export { normalizeToolsConfig, normalizeToolAlias, normalizeToolList, parseToolList, } from "./services/tools-config-normalizer.js";
|
|
45
|
+
export { isHostDeniedFeature } from "./policy/core/feature-decision.js";
|
|
45
46
|
export { frameworkErrorFromToolConfigValidation, normalizeAndValidateToolsConfig, } from "./services/tool-config-validation.js";
|
|
46
47
|
export { createScopedToolId, parseScopedToolId, toOverlayToolId, } from "./services/tool-instance-id.js";
|
|
47
48
|
export { PlaybackState, TTSService } from "./services/TTSService.js";
|
|
@@ -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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a content capability has anything to show, for one entity and one
|
|
3
|
+
* profile.
|
|
4
|
+
*
|
|
5
|
+
* Two independent halves, and a capability is in play only when both answer yes:
|
|
6
|
+
* policy granted one of its support ids, and its own `requiresAuthoredContent`
|
|
7
|
+
* found the resource in this entity's catalogs. Neither implies the other — a
|
|
8
|
+
* learner with an accommodation still sees nothing on an item carrying no
|
|
9
|
+
* resource, and an item carrying one shows nothing to a learner without the
|
|
10
|
+
* grant.
|
|
11
|
+
*
|
|
12
|
+
* Policy answers in three states rather than two, because a host gate is not the
|
|
13
|
+
* absence of a grant: `resolvesWithoutGrant` lets a capability answer from the
|
|
14
|
+
* content when nobody was granted anything, and a host that switched the
|
|
15
|
+
* capability off must not be read as nobody having spoken. Resolution order is
|
|
16
|
+
* denial, then grant, then the content exception.
|
|
17
|
+
*
|
|
18
|
+
* It lives here, data-only and DOM-free, because two renderers ask it. The
|
|
19
|
+
* section player asks continuously: policy and catalogs both change under a
|
|
20
|
+
* mounted card, and it re-resolves to reconcile what is on screen. Print asks
|
|
21
|
+
* once — one learner, one profile, decided before the page exists, with nothing
|
|
22
|
+
* to toggle. A second implementation for the one-shot case would be two
|
|
23
|
+
* renderers disagreeing about the same card, which is the failure this exists to
|
|
24
|
+
* prevent.
|
|
25
|
+
*
|
|
26
|
+
* Capability-neutral, and gated as such: nothing here names a support id, a
|
|
27
|
+
* catalog type, or a surface. The caller passes the registrations and the slot,
|
|
28
|
+
* a capability interprets its own cards, and the resolution owns only the
|
|
29
|
+
* two-halves rule and its one documented exception.
|
|
30
|
+
*/
|
|
31
|
+
import type { ToolRegistration } from "../services/ToolRegistry.js";
|
|
32
|
+
import type { CatalogOwnerSnapshot } from "../services/AccessibilityCatalogResolver.js";
|
|
33
|
+
/** What policy answered about one feature id. */
|
|
34
|
+
export type ContentCapabilityPolicy = {
|
|
35
|
+
outcome: "granted";
|
|
36
|
+
/** The support id that was granted — a capability may declare several. */
|
|
37
|
+
featureId: string;
|
|
38
|
+
/** Feature parameters carried by the decision, if any. */
|
|
39
|
+
parameters?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* No source granted it and none denied it. A capability declaring
|
|
43
|
+
* {@link ToolRegistration.resolvesWithoutGrant} may still answer from the
|
|
44
|
+
* content: silence is what an authored-presentation alternate looks like,
|
|
45
|
+
* since no profile speaks for one either way.
|
|
46
|
+
*/
|
|
47
|
+
| {
|
|
48
|
+
outcome: "silent";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A host gate denied it — the off switch, not the absence of a grant. It
|
|
52
|
+
* outranks `resolvesWithoutGrant`, because a host saying a capability has no
|
|
53
|
+
* place in this delivery is a statement authored content cannot overrule.
|
|
54
|
+
*/
|
|
55
|
+
| {
|
|
56
|
+
outcome: "denied";
|
|
57
|
+
};
|
|
58
|
+
/** Which half of a capability's resolution failed. */
|
|
59
|
+
export type ContentCapabilityPhase = "policy" | "content";
|
|
60
|
+
/** A capability that is in play, and everything its renderer needs to mount it. */
|
|
61
|
+
export interface ResolvedContentCapability {
|
|
62
|
+
registration: ToolRegistration;
|
|
63
|
+
/**
|
|
64
|
+
* The granted support id, or `""` when the capability resolved without a
|
|
65
|
+
* grant. Passed through to the render context unchanged, so a capability that
|
|
66
|
+
* serves both an authored-presentation case and an accommodation can still
|
|
67
|
+
* tell them apart at render time.
|
|
68
|
+
*/
|
|
69
|
+
featureId: string;
|
|
70
|
+
parameters?: unknown;
|
|
71
|
+
/** Whatever the capability's own `resolve` returned; never inspected here. */
|
|
72
|
+
content: unknown;
|
|
73
|
+
}
|
|
74
|
+
export interface ResolveContentCapabilitiesArgs {
|
|
75
|
+
/**
|
|
76
|
+
* The capabilities to consider. A caller with a registry passes
|
|
77
|
+
* `getToolsBySurface(surface)`, which is what keeps a renderer from naming
|
|
78
|
+
* one.
|
|
79
|
+
*/
|
|
80
|
+
registrations: readonly ToolRegistration[];
|
|
81
|
+
/** The entity's cards, or `null` when no resolver is available. */
|
|
82
|
+
catalogs: CatalogOwnerSnapshot | null;
|
|
83
|
+
/**
|
|
84
|
+
* Policy's answer about one feature id, in three states.
|
|
85
|
+
*
|
|
86
|
+
* Granting requires a documented need, so an unconfigured feature is
|
|
87
|
+
* `"silent"`, never granted. What the third state buys is the distinction
|
|
88
|
+
* `"silent"` cannot carry: a host that switched the capability off said
|
|
89
|
+
* something, and a capability allowed to answer from content alone must not
|
|
90
|
+
* treat that as nobody having spoken.
|
|
91
|
+
*/
|
|
92
|
+
policyFor: (featureId: string) => ContentCapabilityPolicy;
|
|
93
|
+
/**
|
|
94
|
+
* Report a capability that threw. It is dropped either way; this is how a
|
|
95
|
+
* caller surfaces it as its own recoverable warning rather than letting one
|
|
96
|
+
* capability's defect reach the learner's content.
|
|
97
|
+
*/
|
|
98
|
+
onError?: (registration: ToolRegistration, phase: ContentCapabilityPhase, error: unknown) => void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The capabilities in play, in the order the caller offered them.
|
|
102
|
+
*
|
|
103
|
+
* Registry order is preserved so a renderer's slot ordering stays a property of
|
|
104
|
+
* the registry rather than of resolution timing.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveContentCapabilities(args: ResolveContentCapabilitiesArgs): ResolvedContentCapability[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a content capability has anything to show, for one entity and one
|
|
3
|
+
* profile.
|
|
4
|
+
*
|
|
5
|
+
* Two independent halves, and a capability is in play only when both answer yes:
|
|
6
|
+
* policy granted one of its support ids, and its own `requiresAuthoredContent`
|
|
7
|
+
* found the resource in this entity's catalogs. Neither implies the other — a
|
|
8
|
+
* learner with an accommodation still sees nothing on an item carrying no
|
|
9
|
+
* resource, and an item carrying one shows nothing to a learner without the
|
|
10
|
+
* grant.
|
|
11
|
+
*
|
|
12
|
+
* Policy answers in three states rather than two, because a host gate is not the
|
|
13
|
+
* absence of a grant: `resolvesWithoutGrant` lets a capability answer from the
|
|
14
|
+
* content when nobody was granted anything, and a host that switched the
|
|
15
|
+
* capability off must not be read as nobody having spoken. Resolution order is
|
|
16
|
+
* denial, then grant, then the content exception.
|
|
17
|
+
*
|
|
18
|
+
* It lives here, data-only and DOM-free, because two renderers ask it. The
|
|
19
|
+
* section player asks continuously: policy and catalogs both change under a
|
|
20
|
+
* mounted card, and it re-resolves to reconcile what is on screen. Print asks
|
|
21
|
+
* once — one learner, one profile, decided before the page exists, with nothing
|
|
22
|
+
* to toggle. A second implementation for the one-shot case would be two
|
|
23
|
+
* renderers disagreeing about the same card, which is the failure this exists to
|
|
24
|
+
* prevent.
|
|
25
|
+
*
|
|
26
|
+
* Capability-neutral, and gated as such: nothing here names a support id, a
|
|
27
|
+
* catalog type, or a surface. The caller passes the registrations and the slot,
|
|
28
|
+
* a capability interprets its own cards, and the resolution owns only the
|
|
29
|
+
* two-halves rule and its one documented exception.
|
|
30
|
+
*/
|
|
31
|
+
/** The support ids a capability answers to, defaulting to its own id. */
|
|
32
|
+
const supportIdsOf = (registration) => registration.pnpSupportIds?.length
|
|
33
|
+
? registration.pnpSupportIds
|
|
34
|
+
: [registration.toolId];
|
|
35
|
+
/**
|
|
36
|
+
* Everything policy has to say about one capability: a grant, an off switch, or
|
|
37
|
+
* nothing.
|
|
38
|
+
*
|
|
39
|
+
* Denial is checked ahead of a grant on each id rather than after the scan,
|
|
40
|
+
* because the two can only disagree when a host blocked one of a capability's ids
|
|
41
|
+
* while a profile granted another, and there the off switch is the later, more
|
|
42
|
+
* specific statement about this delivery.
|
|
43
|
+
*
|
|
44
|
+
* A host gate names *capabilities*, so the tool id is probed too when it is not
|
|
45
|
+
* already a declared support id — otherwise a capability whose id differs from
|
|
46
|
+
* its support ids would slip a host block. That probe is gate-only: a grant on
|
|
47
|
+
* the tool id is ignored, or blocking would double as a second way to switch a
|
|
48
|
+
* capability on.
|
|
49
|
+
*/
|
|
50
|
+
function policyForCapability(registration, args) {
|
|
51
|
+
const supportIds = supportIdsOf(registration);
|
|
52
|
+
for (const supportId of supportIds) {
|
|
53
|
+
const answer = args.policyFor(supportId);
|
|
54
|
+
if (answer.outcome === "denied")
|
|
55
|
+
return { grant: null, denied: true };
|
|
56
|
+
if (answer.outcome === "granted")
|
|
57
|
+
return { grant: answer, denied: false };
|
|
58
|
+
}
|
|
59
|
+
if (supportIds.includes(registration.toolId)) {
|
|
60
|
+
return { grant: null, denied: false };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
grant: null,
|
|
64
|
+
denied: args.policyFor(registration.toolId).outcome === "denied",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveOne(registration, args) {
|
|
68
|
+
let grant = null;
|
|
69
|
+
try {
|
|
70
|
+
const answer = policyForCapability(registration, args);
|
|
71
|
+
// Nothing reopens a host denial — not a grant it outranked, and not the
|
|
72
|
+
// content exception below.
|
|
73
|
+
if (answer.denied)
|
|
74
|
+
return null;
|
|
75
|
+
grant = answer.grant;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
args.onError?.(registration, "policy", error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// The exception, and the only one: a capability whose content can declare
|
|
82
|
+
// itself authored presentation is consulted even when policy granted nothing,
|
|
83
|
+
// because an item family designed to be delivered with its alternate on screen
|
|
84
|
+
// is not an accommodation and no profile grants or revokes it. The capability
|
|
85
|
+
// still decides — `granted` is how it tells the two cases apart.
|
|
86
|
+
if (!grant && !registration.resolvesWithoutGrant)
|
|
87
|
+
return null;
|
|
88
|
+
if (!registration.requiresAuthoredContent) {
|
|
89
|
+
// Nothing to look for in the content, so the grant is the whole answer. A
|
|
90
|
+
// capability reaching here without one has no second half to supply it.
|
|
91
|
+
if (!grant)
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
registration,
|
|
95
|
+
featureId: grant.featureId,
|
|
96
|
+
parameters: grant.parameters,
|
|
97
|
+
content: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const context = {
|
|
101
|
+
featureId: grant?.featureId ?? "",
|
|
102
|
+
parameters: grant?.parameters,
|
|
103
|
+
catalogs: args.catalogs,
|
|
104
|
+
granted: Boolean(grant),
|
|
105
|
+
};
|
|
106
|
+
let content;
|
|
107
|
+
try {
|
|
108
|
+
content = registration.requiresAuthoredContent.resolve(context);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
args.onError?.(registration, "content", error);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Absent content is the honest answer to "is there anything to show", not a
|
|
115
|
+
// failure: the item carries no resource for this capability.
|
|
116
|
+
if (content === null || content === undefined)
|
|
117
|
+
return null;
|
|
118
|
+
return {
|
|
119
|
+
registration,
|
|
120
|
+
featureId: context.featureId,
|
|
121
|
+
parameters: context.parameters,
|
|
122
|
+
content,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The capabilities in play, in the order the caller offered them.
|
|
127
|
+
*
|
|
128
|
+
* Registry order is preserved so a renderer's slot ordering stays a property of
|
|
129
|
+
* the registry rather than of resolution timing.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveContentCapabilities(args) {
|
|
132
|
+
return args.registrations.flatMap((registration) => {
|
|
133
|
+
const resolved = resolveOne(registration, args);
|
|
134
|
+
return resolved ? [resolved] : [];
|
|
135
|
+
});
|
|
136
|
+
}
|
package/dist/tools/internal.d.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
export type { HostedToolContext, HostedToolSize, ResolvedToolContext, ToolActivation, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolProviderDescriptor, ToolRegistration, ToolRenderElement, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolWindowShellConfig, ToolbarContext, } from "../services/ToolRegistry.js";
|
|
22
22
|
export { ToolRegistry } from "../services/ToolRegistry.js";
|
|
23
23
|
export type { CatalogOwnerCard, CatalogOwnerSnapshot, } from "../services/AccessibilityCatalogResolver.js";
|
|
24
|
+
export type { ContentCapabilityPhase, ContentCapabilityPolicy, ResolveContentCapabilitiesArgs, ResolvedContentCapability, } from "./content-capability-resolution.js";
|
|
25
|
+
export { resolveContentCapabilities } from "./content-capability-resolution.js";
|
|
24
26
|
export type { ToolContext, ToolLevel } from "../services/tool-context.js";
|
|
25
27
|
export { hasChoiceInteraction, hasMathContent, hasReadableText, hasScienceContent, } from "../services/tool-context.js";
|
|
26
28
|
export { createScopedToolId } from "../services/tool-instance-id.js";
|
package/dist/tools/internal.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* point — the mechanism is the same one our own registrations use.
|
|
20
20
|
*/
|
|
21
21
|
export { ToolRegistry } from "../services/ToolRegistry.js";
|
|
22
|
+
export { resolveContentCapabilities } from "./content-capability-resolution.js";
|
|
22
23
|
export { hasChoiceInteraction, hasMathContent, hasReadableText, hasScienceContent, } from "../services/tool-context.js";
|
|
23
24
|
// Scoped tool instance ids, so two placements of one tool do not share state.
|
|
24
25
|
export { createScopedToolId } from "../services/tool-instance-id.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-assessment-toolkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.67",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/components/section-toolbar-element.d.ts",
|
|
42
42
|
"import": "./dist/components/section-toolbar-element.js"
|
|
43
43
|
},
|
|
44
|
+
"./services/AccessibilityCatalogResolver": {
|
|
45
|
+
"types": "./dist/services/AccessibilityCatalogResolver.d.ts",
|
|
46
|
+
"import": "./dist/services/AccessibilityCatalogResolver.js"
|
|
47
|
+
},
|
|
44
48
|
"./services/pnp-standard-features": {
|
|
45
49
|
"types": "./dist/services/pnp-standard-features.d.ts",
|
|
46
50
|
"import": "./dist/services/pnp-standard-features.js"
|
|
@@ -79,15 +83,15 @@
|
|
|
79
83
|
"test": "bun test"
|
|
80
84
|
},
|
|
81
85
|
"dependencies": {
|
|
82
|
-
"@pie-players/pie-calculator": "0.3.
|
|
83
|
-
"@pie-players/pie-context": "0.3.
|
|
84
|
-
"@pie-players/pie-players-shared": "0.3.
|
|
85
|
-
"@pie-players/pie-tts": "0.3.
|
|
86
|
+
"@pie-players/pie-calculator": "0.3.67",
|
|
87
|
+
"@pie-players/pie-context": "0.3.67",
|
|
88
|
+
"@pie-players/pie-players-shared": "0.3.67",
|
|
89
|
+
"@pie-players/pie-tts": "0.3.67",
|
|
86
90
|
"speech-rule-engine": "^5.0.0-rc.4"
|
|
87
91
|
},
|
|
88
92
|
"peerDependencies": {
|
|
89
|
-
"@pie-players/pie-calculator-desmos": "0.3.
|
|
90
|
-
"@pie-players/tts-client-server": "0.3.
|
|
93
|
+
"@pie-players/pie-calculator-desmos": "0.3.67",
|
|
94
|
+
"@pie-players/tts-client-server": "0.3.67"
|
|
91
95
|
},
|
|
92
96
|
"peerDependenciesMeta": {
|
|
93
97
|
"@pie-players/pie-calculator-desmos": {
|
|
@@ -100,8 +104,8 @@
|
|
|
100
104
|
"devDependencies": {
|
|
101
105
|
"@biomejs/biome": "^2.5.7",
|
|
102
106
|
"@happy-dom/global-registrator": "^20.11.1",
|
|
103
|
-
"@pie-players/pie-calculator-desmos": "0.3.
|
|
104
|
-
"@pie-players/tts-client-server": "0.3.
|
|
107
|
+
"@pie-players/pie-calculator-desmos": "0.3.67",
|
|
108
|
+
"@pie-players/tts-client-server": "0.3.67",
|
|
105
109
|
"svelte": "^5.56.8",
|
|
106
110
|
"typescript": "^5.9.3"
|
|
107
111
|
},
|