@pie-players/pie-assessment-toolkit 0.3.63 → 0.3.64
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 +63 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/{ItemToolBar-843902tp.js → ItemToolBar-3cppre9r.js} +26 -26
- package/dist/components/chunks/{ItemToolBar-84nv78dy.js → ItemToolBar-7rq2gj8b.js} +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
- package/dist/policy/core/ToolPolicyEngine.js +27 -0
- package/dist/policy/core/feature-decision.d.ts +57 -0
- package/dist/policy/core/feature-decision.js +40 -0
- package/dist/policy/engine.d.ts +1 -0
- package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
- package/dist/policy/sources/PnpPolicySource.js +41 -11
- package/dist/runtime/catalog-registration.d.ts +56 -1
- package/dist/runtime/catalog-registration.js +64 -31
- package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
- package/dist/services/AccessibilityCatalogResolver.js +183 -58
- package/dist/services/SSMLExtractor.js +28 -18
- package/dist/services/TTSService.d.ts +25 -0
- package/dist/services/TTSService.js +241 -45
- package/dist/services/ToolkitCoordinator.d.ts +23 -2
- package/dist/services/ToolkitCoordinator.js +24 -0
- package/dist/services/catalog-media.d.ts +25 -0
- package/dist/services/catalog-media.js +101 -0
- package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -0
- package/dist/services/defaultPersonalNeedsProfile.js +23 -0
- package/dist/services/interfaces.d.ts +29 -2
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/sign-language-cards.d.ts +82 -0
- package/dist/services/sign-language-cards.js +133 -0
- package/dist/services/spoken-audio-cards.d.ts +54 -0
- package/dist/services/spoken-audio-cards.js +66 -0
- package/dist/services/tts/math-aware-text-processing.js +3 -3
- package/dist/services/tts/text-processing.d.ts +51 -0
- package/dist/services/tts/text-processing.js +117 -1
- package/package.json +9 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{B as bJ,C as H0,D as S0,E as pJ,F as L0,G as I0,H as _J,I as d,J as EJ,M as vJ,O as gJ,Q as $J,S as mj,T as hj,b as G0,c as SJ,d as IJ,e as N,f as a,g as w0,
|
|
1
|
+
import{B as bJ,C as H0,D as S0,E as pJ,F as L0,G as I0,H as _J,I as d,J as EJ,M as vJ,O as gJ,Q as $J,S as mj,T as hj,b as G0,c as SJ,d as IJ,e as N,f as a,g as w0,h as P0,i as CJ,j as $0,ja as mJ,k as K0,l as Gj,m as Y0,n as Q0,o as j,q as Pj,r as yJ,s as M0,t as U0,u as F,w as D0,x as Mj,y as e0,z as jj}from"./ItemToolBar-3cppre9r.js";function vj(v){return typeof v.href==="string"}function wJ(v){if(!v||typeof v!=="object"||Array.isArray(v))return!1;let q=v;if(typeof q.id!=="string"||q.id.trim().length===0)return!1;if(typeof q.label!=="string"||q.label.trim().length===0)return!1;if(q.ariaLabel!==void 0&&typeof q.ariaLabel!=="string")return!1;if(q.icon!==void 0&&typeof q.icon!=="string")return!1;if(q.tooltip!==void 0&&typeof q.tooltip!=="string")return!1;if(q.active!==void 0&&typeof q.active!=="boolean")return!1;if(q.disabled!==void 0&&typeof q.disabled!=="boolean")return!1;let s=typeof q.href==="string",m0=typeof q.onClick==="function";if(s===m0)return!1;if(s)return!0;return m0}function gj(v){return typeof v==="string"&&v.trimStart().startsWith("<svg")}function $j(v){return typeof v==="string"&&v.trimStart().startsWith("http")}import{sanitizeSvgIcon as Yj}from"@pie-players/pie-players-shared/security";import{approximateZoomFromWidths as qZ,computeZoomCompensation as FZ,ICON_BUTTON_ZOOM_OPTIONS as HJ}from"@pie-players/pie-players-shared/ui/zoom-compensation";import{createFocusTrap as kZ,FOCUSABLE_SELECTOR as xJ,isProgrammaticFocusTarget as BJ}from"@pie-players/pie-players-shared";import"@pie-players/pie-players-shared/nds-icon-button";var RZ=(v,q)=>{if(customElements.get(v))return;try{customElements.define(v,q)}catch(s){if(!(s instanceof DOMException&&s.name==="NotSupportedError"||s&&typeof s==="object"&&s.name==="NotSupportedError")||!customElements.get(v))throw s}},Oj=M0('<span class="item-toolbar__element-host svelte-rnx3ie"></span>'),WZ=M0('<span class="item-toolbar__nds-button-zoom svelte-rnx3ie"><nds-icon-button></nds-icon-button></span>',2),Nj=M0('<span aria-hidden="true"></span>'),AJ=M0('<img class="item-toolbar__icon-image svelte-rnx3ie" alt=""/>'),fJ=M0('<i aria-hidden="true"></i>'),zZ=M0("<a><!></a>"),wZ=M0('<button type="button"><!></button>'),TJ=M0('<span class="item-toolbar__controls-host svelte-rnx3ie"></span>'),HZ=M0("<div></div>"),xZ=M0('<div><div class="item-toolbar__tools-row svelte-rnx3ie"><!> <!> <!></div> <!></div>'),BZ={hash:"svelte-rnx3ie",code:`.item-toolbar.svelte-rnx3ie {display:flex;flex-direction:column;align-items:flex-end;gap:0;--pie-toolbar-tools-row-height: 2rem;--pie-tts-controls-row-height: 2.875rem;}.item-toolbar__tools-row.svelte-rnx3ie {display:flex;align-items:center;justify-content:flex-end;flex-wrap:nowrap;
|
|
2
2
|
/* Cap the gap between toolbar items (e.g. TTS play ↔ calculator) at its
|
|
3
3
|
200%-zoom size with the same factor the buttons use, so the spacing
|
|
4
4
|
doesn't keep growing past 200% while the buttons themselves freeze. */gap:calc(0.5rem * var(--pie-toolbar-zoom-comp, 1));min-height:var(--pie-toolbar-tools-row-height);}.item-toolbar__controls-row.svelte-rnx3ie {display:flex;align-items:center;justify-content:flex-end;width:100%;min-height:0;height:auto;}.item-toolbar__controls-row--reserve.svelte-rnx3ie {min-height:var(--pie-tts-controls-row-height);height:var(--pie-tts-controls-row-height);}.item-toolbar__controls-row--active.svelte-rnx3ie {min-height:var(--pie-tts-controls-row-height);height:var(--pie-tts-controls-row-height);}.item-toolbar__controls-row--align-start.svelte-rnx3ie {justify-content:flex-start;}.item-toolbar__controls-host.svelte-rnx3ie {display:inline-flex;align-items:center;justify-content:flex-end;width:100%;}.item-toolbar--top.svelte-rnx3ie,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ export type { AssessmentToolkitHostRuntimeContext, AssessmentToolkitRegionScopeC
|
|
|
10
10
|
export type { TTSHighlightContext, TTSHighlightTargetResolver, TTSHighlightTargetResolverProvider, TTSHighlightTargetResolverRuntime, } from "./services/tts/highlight-target-resolver.js";
|
|
11
11
|
export { assessmentToolkitHostRuntimeContext, assessmentToolkitRegionScopeContext, assessmentToolkitRuntimeContext, assessmentToolkitShellContext, } from "./context/assessment-toolkit-context.js";
|
|
12
12
|
export { connectAssessmentToolkitHostRuntimeContext, connectAssessmentToolkitRegionScopeContext, connectAssessmentToolkitRuntimeContext, connectAssessmentToolkitShellContext, } from "./context/runtime-context-consumer.js";
|
|
13
|
+
export { catalogOwnerContextFor, collectCatalogRegistrations, collectEntityCatalogRegistrations, type CatalogOwnerIdentity, type CatalogRegistration, type CatalogSourceEntity, } from "./runtime/catalog-registration.js";
|
|
13
14
|
export { PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, PIE_REGISTER_EVENT, PIE_UNREGISTER_EVENT, type InternalContentLoadedDetail, type InternalItemSessionChangedDetail, type InternalItemPlayerErrorDetail, type ItemSessionChangedDetail, type RuntimeRegistrationDetail, type RuntimeRegistrationKind, } from "./runtime/registration-events.js";
|
|
14
15
|
export { connectToolRegionScopeContext, connectToolRuntimeContext, connectToolShellContext, createCrossBoundaryEvent, dispatchCrossBoundaryEvent, isContextValueDefined, } from "./runtime/tool-host-contract.js";
|
|
15
16
|
export type { AccessibilityCatalogResolverApi, ElementToolStateStoreApi, HighlightCoordinatorApi, I18nServiceApi, ThemeProviderApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi, ToolState, } from "./services/interfaces.js";
|
|
16
|
-
export type { CatalogLookupContext, CatalogLookupOptions, CatalogOwnerContext, CatalogOwnerKind, CatalogStatistics, CatalogType, ResolvedCatalog, } from "./services/AccessibilityCatalogResolver.js";
|
|
17
|
-
export { AccessibilityCatalogResolver } from "./services/AccessibilityCatalogResolver.js";
|
|
17
|
+
export type { CatalogCardForm, CatalogChangeEvent, CatalogChangeListener, CatalogChangeReason, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerContext, CatalogOwnerKind, CatalogStatistics, CatalogType, ResolvedCatalog, } from "./services/AccessibilityCatalogResolver.js";
|
|
18
|
+
export { AccessibilityCatalogResolver, catalogCardForm, isKnownCatalogType, KNOWN_CATALOG_TYPES, } from "./services/AccessibilityCatalogResolver.js";
|
|
18
19
|
export { ContextVariableStore } from "./services/ContextVariableStore.js";
|
|
19
20
|
export { ElementToolStateStore } from "./services/ElementToolStateStore.js";
|
|
20
21
|
export type { Annotation } from "./services/HighlightCoordinator.js";
|
|
@@ -30,10 +31,14 @@ export { extractTextContent, hasChoiceInteraction, hasMathContent, hasReadableTe
|
|
|
30
31
|
export { DEFAULT_TOOL_PLACEMENT, DEFAULT_TOOL_ORDER, createPackagedToolRegistry, PACKAGED_TOOL_PLACEMENT, registerPackagedTools, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./services/createDefaultToolRegistry.js";
|
|
31
32
|
export type { CreateToolsConfigArgs } from "./services/create-tools-config.js";
|
|
32
33
|
export { createToolsConfig } from "./services/create-tools-config.js";
|
|
33
|
-
export { DEFAULT_PERSONAL_NEEDS_PROFILE, createDefaultPersonalNeedsProfile, } from "./services/defaultPersonalNeedsProfile.js";
|
|
34
|
+
export { ACCOMMODATION_ONLY_SUPPORT_IDS, DEFAULT_PERSONAL_NEEDS_PROFILE, createDefaultPersonalNeedsProfile, } from "./services/defaultPersonalNeedsProfile.js";
|
|
34
35
|
export type { ToolComponentFactory, ToolComponentFactoryMap, ToolComponentOverrides, ToolTagMap, } from "./tools/tool-tag-map.js";
|
|
35
36
|
export { createToolElement, DEFAULT_TOOL_TAG_MAP, resolveToolTag, toToolIdFromTag, } from "./tools/tool-tag-map.js";
|
|
36
37
|
export { ALL_STANDARD_ACCESS_FEATURES, EXAMPLE_PNP_CONFIGURATIONS, getFeatureCategory, getFeaturesInCategory, isStandardAccessFeature, QTI_STANDARD_ACCESS_FEATURES, } from "./services/pnp-standard-features.js";
|
|
38
|
+
export type { SignLanguageMedia } from "./services/sign-language-cards.js";
|
|
39
|
+
export { AMERICAN_SIGN_LANGUAGE, applyMediaFragment, describeSignLanguage, isSignLanguageCard, matchesRequestedSignLanguage, resolveSignLanguageMedia, SIGN_LANGUAGE_CATALOG_TYPE, } from "./services/sign-language-cards.js";
|
|
40
|
+
export type { SpokenAudioMedia } from "./services/spoken-audio-cards.js";
|
|
41
|
+
export { resolveSpokenAudioMedia, SPOKEN_CATALOG_TYPE, } from "./services/spoken-audio-cards.js";
|
|
37
42
|
export type { ExtractionResult } from "./services/SSMLExtractor.js";
|
|
38
43
|
export { SSMLExtractor } from "./services/SSMLExtractor.js";
|
|
39
44
|
export type { FontSize, ThemeConfig } from "./services/ThemeProvider.js";
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { assessmentToolkitHostRuntimeContext, assessmentToolkitRegionScopeContext, assessmentToolkitRuntimeContext, assessmentToolkitShellContext, } from "./context/assessment-toolkit-context.js";
|
|
10
10
|
export { connectAssessmentToolkitHostRuntimeContext, connectAssessmentToolkitRegionScopeContext, connectAssessmentToolkitRuntimeContext, connectAssessmentToolkitShellContext, } from "./context/runtime-context-consumer.js";
|
|
11
|
+
export { catalogOwnerContextFor, collectCatalogRegistrations, collectEntityCatalogRegistrations, } from "./runtime/catalog-registration.js";
|
|
11
12
|
export { PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, PIE_REGISTER_EVENT, PIE_UNREGISTER_EVENT, } from "./runtime/registration-events.js";
|
|
12
13
|
export { connectToolRegionScopeContext, connectToolRuntimeContext, connectToolShellContext, createCrossBoundaryEvent, dispatchCrossBoundaryEvent, isContextValueDefined, } from "./runtime/tool-host-contract.js";
|
|
13
|
-
export { AccessibilityCatalogResolver } from "./services/AccessibilityCatalogResolver.js";
|
|
14
|
+
export { AccessibilityCatalogResolver, catalogCardForm, isKnownCatalogType, KNOWN_CATALOG_TYPES, } from "./services/AccessibilityCatalogResolver.js";
|
|
14
15
|
// Context Variable Store (QTI 3.0 Context Declarations)
|
|
15
16
|
export { ContextVariableStore } from "./services/ContextVariableStore.js";
|
|
16
17
|
// Element Tool State Store (Element-level ephemeral tool state)
|
|
@@ -22,10 +23,12 @@ export { ToolRegistry } from "./services/ToolRegistry.js";
|
|
|
22
23
|
export { extractTextContent, hasChoiceInteraction, hasMathContent, hasReadableText, isAssessmentContext, isElementContext, isItemContext, isPassageContext, isRubricContext, isSectionContext, } from "./services/tool-context.js";
|
|
23
24
|
export { DEFAULT_TOOL_PLACEMENT, DEFAULT_TOOL_ORDER, createPackagedToolRegistry, PACKAGED_TOOL_PLACEMENT, registerPackagedTools, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./services/createDefaultToolRegistry.js";
|
|
24
25
|
export { createToolsConfig } from "./services/create-tools-config.js";
|
|
25
|
-
export { DEFAULT_PERSONAL_NEEDS_PROFILE, createDefaultPersonalNeedsProfile, } from "./services/defaultPersonalNeedsProfile.js";
|
|
26
|
+
export { ACCOMMODATION_ONLY_SUPPORT_IDS, DEFAULT_PERSONAL_NEEDS_PROFILE, createDefaultPersonalNeedsProfile, } from "./services/defaultPersonalNeedsProfile.js";
|
|
26
27
|
export { createToolElement, DEFAULT_TOOL_TAG_MAP, resolveToolTag, toToolIdFromTag, } from "./tools/tool-tag-map.js";
|
|
27
28
|
// QTI 3.0 Standard Access Features
|
|
28
29
|
export { ALL_STANDARD_ACCESS_FEATURES, EXAMPLE_PNP_CONFIGURATIONS, getFeatureCategory, getFeaturesInCategory, isStandardAccessFeature, QTI_STANDARD_ACCESS_FEATURES, } from "./services/pnp-standard-features.js";
|
|
30
|
+
export { AMERICAN_SIGN_LANGUAGE, applyMediaFragment, describeSignLanguage, isSignLanguageCard, matchesRequestedSignLanguage, resolveSignLanguageMedia, SIGN_LANGUAGE_CATALOG_TYPE, } from "./services/sign-language-cards.js";
|
|
31
|
+
export { resolveSpokenAudioMedia, SPOKEN_CATALOG_TYPE, } from "./services/spoken-audio-cards.js";
|
|
29
32
|
export { SSMLExtractor } from "./services/SSMLExtractor.js";
|
|
30
33
|
export { ThemeProvider } from "./services/ThemeProvider.js";
|
|
31
34
|
// Tool Coordinator
|
|
@@ -21,6 +21,7 @@ import type { CanonicalToolsConfig, ToolPlacementLevel } from "../../services/to
|
|
|
21
21
|
import type { ToolRegistry } from "../../services/ToolRegistry.js";
|
|
22
22
|
import type { ToolPolicyDecision, ToolPolicyDecisionRequest } from "./decision-types.js";
|
|
23
23
|
import type { PolicySource } from "./PolicySource.js";
|
|
24
|
+
import type { FeaturePolicyDecision } from "./feature-decision.js";
|
|
24
25
|
export type PnpEnforcementMode = "on" | "off";
|
|
25
26
|
export interface ToolPolicyEngineInputs {
|
|
26
27
|
tools?: CanonicalToolsConfig | null;
|
|
@@ -88,6 +89,26 @@ export declare class ToolPolicyEngine {
|
|
|
88
89
|
* cached `ToolPolicyDecision` rather than re-call `decide(...)`.
|
|
89
90
|
*/
|
|
90
91
|
decide(request: ToolPolicyDecisionRequest): ToolPolicyDecision;
|
|
92
|
+
/**
|
|
93
|
+
* Resolve eligibility for one PNP/AfA feature id through the six-level
|
|
94
|
+
* precedence, independent of toolbar placement.
|
|
95
|
+
*
|
|
96
|
+
* For capabilities that render as their own surface rather than a toolbar
|
|
97
|
+
* button — a signed alternate's region, for example — `decide(...)` cannot
|
|
98
|
+
* answer the question, because such a capability is deliberately absent from
|
|
99
|
+
* `tools.placement` and would read as "removed by policy" when nothing of
|
|
100
|
+
* the sort happened. See {@link FeaturePolicyDecision}.
|
|
101
|
+
*
|
|
102
|
+
* `pnpEnforcement` is deliberately **not** consulted here. That flag governs
|
|
103
|
+
* whether PNP/profile policy *refines* an otherwise-visible tool set; a
|
|
104
|
+
* feature with no placement has no unrefined baseline to fall back to, so
|
|
105
|
+
* skipping the PNP read would make the capability permanently unavailable
|
|
106
|
+
* rather than merely unrefined. Auto-mode already flips enforcement on
|
|
107
|
+
* whenever profile material exists, so this only diverges for a host that
|
|
108
|
+
* explicitly forces `"off"` while supplying a profile that grants the
|
|
109
|
+
* feature — and there, honouring the profile is the safer failure.
|
|
110
|
+
*/
|
|
111
|
+
decideFeature(featureId: string): FeaturePolicyDecision;
|
|
91
112
|
/**
|
|
92
113
|
* Convenience wrapper for hosts that just want the visible tool
|
|
93
114
|
* IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `ToolkitCoordinator`. PR 3 switches `<pie-item-toolbar>` over.
|
|
18
18
|
*/
|
|
19
19
|
import { normalizeToolsConfig } from "../../services/tools-config-normalizer.js";
|
|
20
|
+
import { interpretFeatureResult } from "./feature-decision.js";
|
|
20
21
|
import { composeDecision } from "./compose-decision.js";
|
|
21
22
|
import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
|
|
22
23
|
import { PnpPolicySource } from "../sources/PnpPolicySource.js";
|
|
@@ -85,6 +86,32 @@ export class ToolPolicyEngine {
|
|
|
85
86
|
contextId: requestContextId,
|
|
86
87
|
});
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve eligibility for one PNP/AfA feature id through the six-level
|
|
91
|
+
* precedence, independent of toolbar placement.
|
|
92
|
+
*
|
|
93
|
+
* For capabilities that render as their own surface rather than a toolbar
|
|
94
|
+
* button — a signed alternate's region, for example — `decide(...)` cannot
|
|
95
|
+
* answer the question, because such a capability is deliberately absent from
|
|
96
|
+
* `tools.placement` and would read as "removed by policy" when nothing of
|
|
97
|
+
* the sort happened. See {@link FeaturePolicyDecision}.
|
|
98
|
+
*
|
|
99
|
+
* `pnpEnforcement` is deliberately **not** consulted here. That flag governs
|
|
100
|
+
* whether PNP/profile policy *refines* an otherwise-visible tool set; a
|
|
101
|
+
* feature with no placement has no unrefined baseline to fall back to, so
|
|
102
|
+
* skipping the PNP read would make the capability permanently unavailable
|
|
103
|
+
* rather than merely unrefined. Auto-mode already flips enforcement on
|
|
104
|
+
* whenever profile material exists, so this only diverges for a host that
|
|
105
|
+
* explicitly forces `"off"` while supplying a profile that grants the
|
|
106
|
+
* feature — and there, honouring the profile is the safer failure.
|
|
107
|
+
*/
|
|
108
|
+
decideFeature(featureId) {
|
|
109
|
+
this.assertNotDisposed();
|
|
110
|
+
return interpretFeatureResult(featureId, this.pnpPolicySource.resolveFeature(featureId, {
|
|
111
|
+
assessment: this.assessment ?? undefined,
|
|
112
|
+
currentItemRef: this.currentItemRef ?? undefined,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
88
115
|
/**
|
|
89
116
|
* Convenience wrapper for hosts that just want the visible tool
|
|
90
117
|
* IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature policy decisions — eligibility for policy-addressable capabilities
|
|
3
|
+
* that are not toolbar tools.
|
|
4
|
+
*
|
|
5
|
+
* "Tool" in this codebase means *policy-addressable capability*, not "gadget on
|
|
6
|
+
* a toolbar" (see `docs/tools-and-accomodations/architecture.md`). Most such
|
|
7
|
+
* capabilities render as toolbar surfaces, so the engine's main answer is a
|
|
8
|
+
* placement-scoped `ToolPolicyDecision`. Some do not: a signed alternate renders
|
|
9
|
+
* as its own region and is never placed on a toolbar, so asking
|
|
10
|
+
* `decide({ level: "item", ... })` about it would answer the wrong question —
|
|
11
|
+
* it would be absent because it is not in `tools.placement.item`, not because
|
|
12
|
+
* policy said no.
|
|
13
|
+
*
|
|
14
|
+
* A feature decision answers only the eligibility half: *did policy grant this
|
|
15
|
+
* feature id?* Whether the capability has anything to show is a separate,
|
|
16
|
+
* independent check owned by its renderer — for signing, whether a matching
|
|
17
|
+
* catalog card exists. Both are required; neither implies the other.
|
|
18
|
+
*/
|
|
19
|
+
import type { PnpPolicyResult } from "../sources/PnpPolicySource.js";
|
|
20
|
+
import type { PnpPolicySourceRule } from "./policy-source-tag.js";
|
|
21
|
+
import type { ToolPolicyResolutionDecision, ToolPolicySourceType } from "./provenance.js";
|
|
22
|
+
export interface FeaturePolicyDecision {
|
|
23
|
+
/** The PNP/AfA support id that was evaluated (e.g. `"signLanguage"`). */
|
|
24
|
+
featureId: string;
|
|
25
|
+
/**
|
|
26
|
+
* `true` only when policy explicitly granted the feature at one of the six
|
|
27
|
+
* precedence levels. A feature nobody configured is **not** available —
|
|
28
|
+
* accommodations require a documented need, so silence means no.
|
|
29
|
+
*/
|
|
30
|
+
granted: boolean;
|
|
31
|
+
action: ToolPolicyResolutionDecision["action"];
|
|
32
|
+
/** Which precedence rule produced the verdict. */
|
|
33
|
+
rule: PnpPolicySourceRule;
|
|
34
|
+
precedence: 1 | 2 | 3 | 4 | 5 | 6;
|
|
35
|
+
sourceType: ToolPolicySourceType;
|
|
36
|
+
/** Human-readable explanation, suitable for a policy debugger. */
|
|
37
|
+
reason: string;
|
|
38
|
+
/**
|
|
39
|
+
* `true` when the grant is a mandate (item or district `requiredTools`)
|
|
40
|
+
* rather than a student-profile support.
|
|
41
|
+
*/
|
|
42
|
+
required: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Feature parameters resolved from item `toolParameters` then assessment
|
|
45
|
+
* `toolConfigs`, keyed by the feature id. The seam a later configurable
|
|
46
|
+
* presentation would hang on; no vocabulary is defined yet.
|
|
47
|
+
*/
|
|
48
|
+
parameters?: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
|
|
52
|
+
*
|
|
53
|
+
* `resolveFeature` evaluates exactly one support id, so the result carries
|
|
54
|
+
* exactly one decision and at most one flags entry — no mapped-tool-id
|
|
55
|
+
* bookkeeping is needed to read it back out.
|
|
56
|
+
*/
|
|
57
|
+
export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult): FeaturePolicyDecision;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature policy decisions — eligibility for policy-addressable capabilities
|
|
3
|
+
* that are not toolbar tools.
|
|
4
|
+
*
|
|
5
|
+
* "Tool" in this codebase means *policy-addressable capability*, not "gadget on
|
|
6
|
+
* a toolbar" (see `docs/tools-and-accomodations/architecture.md`). Most such
|
|
7
|
+
* capabilities render as toolbar surfaces, so the engine's main answer is a
|
|
8
|
+
* placement-scoped `ToolPolicyDecision`. Some do not: a signed alternate renders
|
|
9
|
+
* as its own region and is never placed on a toolbar, so asking
|
|
10
|
+
* `decide({ level: "item", ... })` about it would answer the wrong question —
|
|
11
|
+
* it would be absent because it is not in `tools.placement.item`, not because
|
|
12
|
+
* policy said no.
|
|
13
|
+
*
|
|
14
|
+
* A feature decision answers only the eligibility half: *did policy grant this
|
|
15
|
+
* feature id?* Whether the capability has anything to show is a separate,
|
|
16
|
+
* independent check owned by its renderer — for signing, whether a matching
|
|
17
|
+
* catalog card exists. Both are required; neither implies the other.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
|
|
21
|
+
*
|
|
22
|
+
* `resolveFeature` evaluates exactly one support id, so the result carries
|
|
23
|
+
* exactly one decision and at most one flags entry — no mapped-tool-id
|
|
24
|
+
* bookkeeping is needed to read it back out.
|
|
25
|
+
*/
|
|
26
|
+
export function interpretFeatureResult(featureId, result) {
|
|
27
|
+
const decision = result.decisions[0];
|
|
28
|
+
const flags = Array.from(result.perToolFlags.values())[0];
|
|
29
|
+
return {
|
|
30
|
+
featureId,
|
|
31
|
+
granted: decision?.action === "enable",
|
|
32
|
+
action: decision?.action ?? "skip",
|
|
33
|
+
rule: decision?.rule ?? "pnp-support",
|
|
34
|
+
precedence: decision?.precedence ?? 6,
|
|
35
|
+
sourceType: decision?.sourceType ?? "system",
|
|
36
|
+
reason: decision?.reason ?? `Feature "${featureId}" not configured`,
|
|
37
|
+
required: Boolean(flags?.required),
|
|
38
|
+
parameters: flags?.settings,
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/policy/engine.d.ts
CHANGED
|
@@ -21,6 +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 { FeaturePolicyDecision } from "./core/feature-decision.js";
|
|
24
25
|
export type { RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, } from "./core/decision-types.js";
|
|
25
26
|
export type { PolicySource, PolicySourceDecisionContext, PolicySourceProvenanceEntry, PolicySourceResult, } from "./core/PolicySource.js";
|
|
26
27
|
export type { PolicySourceTag, PnpPolicySourceRule, PnpPolicySourceTag, CustomPolicySourceTag, } from "./core/policy-source-tag.js";
|
|
@@ -88,6 +88,28 @@ export declare class PnpPolicySource {
|
|
|
88
88
|
private readonly toolRegistry;
|
|
89
89
|
constructor(toolRegistry: ToolRegistry);
|
|
90
90
|
apply(args: PnpPolicyApplyArgs): PnpPolicyResult;
|
|
91
|
+
/**
|
|
92
|
+
* Evaluate exactly one PNP support id through the same six-level
|
|
93
|
+
* precedence `apply(...)` uses.
|
|
94
|
+
*
|
|
95
|
+
* This exists for **policy-addressable capabilities that are not toolbar
|
|
96
|
+
* tools** — a signed alternate rendered as its own region, for example.
|
|
97
|
+
* `apply(...)` only evaluates support ids that appear somewhere in the
|
|
98
|
+
* bound policy inputs, and it keys its result maps by *mapped tool id*, so
|
|
99
|
+
* a caller asking about one feature would have to re-derive that mapping
|
|
100
|
+
* and could collide with another support id that maps to the same tool.
|
|
101
|
+
* Evaluating one id in isolation avoids both problems: the returned result
|
|
102
|
+
* carries exactly one decision, and `decisions[0].action` is the verdict.
|
|
103
|
+
*
|
|
104
|
+
* Reusing `resolveSupport(...)` rather than re-walking the precedence rules
|
|
105
|
+
* is the point — a second copy of the six levels would drift.
|
|
106
|
+
*/
|
|
107
|
+
resolveFeature(featureId: string, args: PnpPolicyApplyArgs): PnpPolicyResult;
|
|
108
|
+
/**
|
|
109
|
+
* Build the rule-evaluation context and the empty result (with its
|
|
110
|
+
* configuration-source attribution) from the bound policy inputs.
|
|
111
|
+
*/
|
|
112
|
+
private prepare;
|
|
91
113
|
private resolveSupport;
|
|
92
114
|
/**
|
|
93
115
|
* Map a QTI / PNP support id (e.g. `"calculator-basic"`) to the
|
|
@@ -14,6 +14,46 @@ export class PnpPolicySource {
|
|
|
14
14
|
this.toolRegistry = toolRegistry;
|
|
15
15
|
}
|
|
16
16
|
apply(args) {
|
|
17
|
+
const { ctx, result } = this.prepare(args);
|
|
18
|
+
const { pnp, districtPolicy, itemSettings } = ctx;
|
|
19
|
+
const allSupports = new Set();
|
|
20
|
+
pnp?.supports?.forEach((s) => allSupports.add(s));
|
|
21
|
+
pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
|
|
22
|
+
districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
|
|
23
|
+
districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
|
|
24
|
+
itemSettings?.requiredTools?.forEach((s) => allSupports.add(s));
|
|
25
|
+
itemSettings?.restrictedTools?.forEach((s) => allSupports.add(s));
|
|
26
|
+
for (const supportId of allSupports) {
|
|
27
|
+
this.resolveSupport(supportId, ctx, result);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Evaluate exactly one PNP support id through the same six-level
|
|
33
|
+
* precedence `apply(...)` uses.
|
|
34
|
+
*
|
|
35
|
+
* This exists for **policy-addressable capabilities that are not toolbar
|
|
36
|
+
* tools** — a signed alternate rendered as its own region, for example.
|
|
37
|
+
* `apply(...)` only evaluates support ids that appear somewhere in the
|
|
38
|
+
* bound policy inputs, and it keys its result maps by *mapped tool id*, so
|
|
39
|
+
* a caller asking about one feature would have to re-derive that mapping
|
|
40
|
+
* and could collide with another support id that maps to the same tool.
|
|
41
|
+
* Evaluating one id in isolation avoids both problems: the returned result
|
|
42
|
+
* carries exactly one decision, and `decisions[0].action` is the verdict.
|
|
43
|
+
*
|
|
44
|
+
* Reusing `resolveSupport(...)` rather than re-walking the precedence rules
|
|
45
|
+
* is the point — a second copy of the six levels would drift.
|
|
46
|
+
*/
|
|
47
|
+
resolveFeature(featureId, args) {
|
|
48
|
+
const { ctx, result } = this.prepare(args);
|
|
49
|
+
this.resolveSupport(featureId, ctx, result);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the rule-evaluation context and the empty result (with its
|
|
54
|
+
* configuration-source attribution) from the bound policy inputs.
|
|
55
|
+
*/
|
|
56
|
+
prepare(args) {
|
|
17
57
|
const { assessment, currentItemRef } = args;
|
|
18
58
|
const pnp = assessment?.personalNeedsProfile;
|
|
19
59
|
const settings = assessment?.settings;
|
|
@@ -46,13 +86,6 @@ export class PnpPolicySource {
|
|
|
46
86
|
config: itemSettings,
|
|
47
87
|
};
|
|
48
88
|
}
|
|
49
|
-
const allSupports = new Set();
|
|
50
|
-
pnp?.supports?.forEach((s) => allSupports.add(s));
|
|
51
|
-
pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
|
|
52
|
-
settings?.districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
|
|
53
|
-
settings?.districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
|
|
54
|
-
itemSettings?.requiredTools?.forEach((s) => allSupports.add(s));
|
|
55
|
-
itemSettings?.restrictedTools?.forEach((s) => allSupports.add(s));
|
|
56
89
|
const ctx = {
|
|
57
90
|
pnp,
|
|
58
91
|
districtPolicy: settings?.districtPolicy,
|
|
@@ -60,10 +93,7 @@ export class PnpPolicySource {
|
|
|
60
93
|
itemSettings,
|
|
61
94
|
toolConfigs: settings?.toolConfigs,
|
|
62
95
|
};
|
|
63
|
-
|
|
64
|
-
this.resolveSupport(supportId, ctx, result);
|
|
65
|
-
}
|
|
66
|
-
return result;
|
|
96
|
+
return { ctx, result };
|
|
67
97
|
}
|
|
68
98
|
resolveSupport(supportId, ctx, out) {
|
|
69
99
|
// 1. District block (absolute veto)
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where accessibility catalogs live on a rendered entity, and which owner scope
|
|
3
|
+
* each one belongs to.
|
|
4
|
+
*
|
|
5
|
+
* Catalogs are placed dynamically: a shell registers what its entity carries
|
|
6
|
+
* when it mounts, and readers (TTS, the item card's media region) resolve by
|
|
7
|
+
* identifier within an owner scope. Both sides therefore have to agree on two
|
|
8
|
+
* facts — the three places catalogs can hang off an entity, and the owner
|
|
9
|
+
* context each one is filed under. This module is the only place either is
|
|
10
|
+
* decided, so a reader cannot look up a scope registration never wrote.
|
|
11
|
+
*
|
|
12
|
+
* Part of PIE Assessment Toolkit.
|
|
13
|
+
*/
|
|
1
14
|
import type { AccessibilityCatalog } from "@pie-players/pie-players-shared/types";
|
|
2
15
|
import type { CatalogOwnerContext } from "../services/AccessibilityCatalogResolver.js";
|
|
3
|
-
import type { RuntimeRegistrationDetail } from "./registration-events.js";
|
|
16
|
+
import type { RuntimeRegistrationDetail, RuntimeRegistrationKind } from "./registration-events.js";
|
|
4
17
|
export interface CatalogRegistrationRuntimeContext {
|
|
5
18
|
assessmentId?: string;
|
|
6
19
|
sectionId?: string;
|
|
@@ -9,4 +22,46 @@ export interface CatalogRegistration {
|
|
|
9
22
|
context: CatalogOwnerContext;
|
|
10
23
|
catalogs: AccessibilityCatalog[];
|
|
11
24
|
}
|
|
25
|
+
/** The entity shape catalogs hang off: an item, or a passage. */
|
|
26
|
+
export interface CatalogSourceEntity {
|
|
27
|
+
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
28
|
+
config?: {
|
|
29
|
+
extractedCatalogs?: AccessibilityCatalog[];
|
|
30
|
+
models?: Array<{
|
|
31
|
+
id?: string;
|
|
32
|
+
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Who is rendering the entity — everything owner scoping is derived from. */
|
|
37
|
+
export interface CatalogOwnerIdentity {
|
|
38
|
+
kind: RuntimeRegistrationKind;
|
|
39
|
+
/** The rendered instance id. */
|
|
40
|
+
itemId: string;
|
|
41
|
+
canonicalItemId?: string;
|
|
42
|
+
assessmentId?: string;
|
|
43
|
+
sectionId?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The owner context an entity's catalogs are registered under, and therefore the
|
|
47
|
+
* one a reader must look them up with.
|
|
48
|
+
*
|
|
49
|
+
* Exported because readers construct the lookup context themselves: the
|
|
50
|
+
* resolver matches contexts field by field, so a reader that hand-assembled its
|
|
51
|
+
* own would silently resolve nothing the day either side gained a field.
|
|
52
|
+
*/
|
|
53
|
+
export declare function catalogOwnerContextFor(owner: CatalogOwnerIdentity): CatalogOwnerContext;
|
|
54
|
+
/**
|
|
55
|
+
* Every catalog an entity carries, paired with the owner scope it belongs in.
|
|
56
|
+
*
|
|
57
|
+
* Three places carry catalogs, and the distinction matters to resolution rather
|
|
58
|
+
* than only to bookkeeping: entity-level `accessibilityCatalogs` and
|
|
59
|
+
* extractor-generated `config.extractedCatalogs` are filed against the entity,
|
|
60
|
+
* while a model's own catalogs are filed against that model, so two models on
|
|
61
|
+
* one item can use the same catalog identifier without colliding.
|
|
62
|
+
*
|
|
63
|
+
* Passages have no models, so the walk stops after the entity-level pair.
|
|
64
|
+
*/
|
|
65
|
+
export declare function collectEntityCatalogRegistrations(entity: CatalogSourceEntity | null | undefined, owner: CatalogOwnerIdentity): CatalogRegistration[];
|
|
66
|
+
/** Adapter for the runtime registration event a shell dispatches on mount. */
|
|
12
67
|
export declare function collectCatalogRegistrations(detail: RuntimeRegistrationDetail, runtime?: CatalogRegistrationRuntimeContext): CatalogRegistration[];
|
|
@@ -1,53 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where accessibility catalogs live on a rendered entity, and which owner scope
|
|
3
|
+
* each one belongs to.
|
|
4
|
+
*
|
|
5
|
+
* Catalogs are placed dynamically: a shell registers what its entity carries
|
|
6
|
+
* when it mounts, and readers (TTS, the item card's media region) resolve by
|
|
7
|
+
* identifier within an owner scope. Both sides therefore have to agree on two
|
|
8
|
+
* facts — the three places catalogs can hang off an entity, and the owner
|
|
9
|
+
* context each one is filed under. This module is the only place either is
|
|
10
|
+
* decided, so a reader cannot look up a scope registration never wrote.
|
|
11
|
+
*
|
|
12
|
+
* Part of PIE Assessment Toolkit.
|
|
13
|
+
*/
|
|
1
14
|
const hasCatalogs = (catalogs) => Array.isArray(catalogs) && catalogs.length > 0;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The owner context an entity's catalogs are registered under, and therefore the
|
|
17
|
+
* one a reader must look them up with.
|
|
18
|
+
*
|
|
19
|
+
* Exported because readers construct the lookup context themselves: the
|
|
20
|
+
* resolver matches contexts field by field, so a reader that hand-assembled its
|
|
21
|
+
* own would silently resolve nothing the day either side gained a field.
|
|
22
|
+
*/
|
|
23
|
+
export function catalogOwnerContextFor(owner) {
|
|
24
|
+
if (owner.kind === "passage") {
|
|
25
|
+
return {
|
|
9
26
|
ownerKind: "passage",
|
|
10
|
-
assessmentId:
|
|
11
|
-
sectionId:
|
|
12
|
-
passageId:
|
|
27
|
+
assessmentId: owner.assessmentId,
|
|
28
|
+
sectionId: owner.sectionId,
|
|
29
|
+
passageId: owner.canonicalItemId || owner.itemId,
|
|
13
30
|
};
|
|
14
|
-
if (hasCatalogs(entity.accessibilityCatalogs)) {
|
|
15
|
-
registrations.push({ context, catalogs: entity.accessibilityCatalogs });
|
|
16
|
-
}
|
|
17
|
-
if (hasCatalogs(entity.config?.extractedCatalogs)) {
|
|
18
|
-
registrations.push({
|
|
19
|
-
context,
|
|
20
|
-
catalogs: entity.config.extractedCatalogs,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return registrations;
|
|
24
31
|
}
|
|
25
|
-
|
|
32
|
+
return {
|
|
26
33
|
ownerKind: "itemModel",
|
|
27
|
-
assessmentId:
|
|
28
|
-
sectionId:
|
|
29
|
-
itemId:
|
|
30
|
-
canonicalItemId:
|
|
34
|
+
assessmentId: owner.assessmentId,
|
|
35
|
+
sectionId: owner.sectionId,
|
|
36
|
+
itemId: owner.itemId,
|
|
37
|
+
canonicalItemId: owner.canonicalItemId || owner.itemId,
|
|
31
38
|
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every catalog an entity carries, paired with the owner scope it belongs in.
|
|
42
|
+
*
|
|
43
|
+
* Three places carry catalogs, and the distinction matters to resolution rather
|
|
44
|
+
* than only to bookkeeping: entity-level `accessibilityCatalogs` and
|
|
45
|
+
* extractor-generated `config.extractedCatalogs` are filed against the entity,
|
|
46
|
+
* while a model's own catalogs are filed against that model, so two models on
|
|
47
|
+
* one item can use the same catalog identifier without colliding.
|
|
48
|
+
*
|
|
49
|
+
* Passages have no models, so the walk stops after the entity-level pair.
|
|
50
|
+
*/
|
|
51
|
+
export function collectEntityCatalogRegistrations(entity, owner) {
|
|
52
|
+
if (!entity)
|
|
53
|
+
return [];
|
|
54
|
+
const context = catalogOwnerContextFor(owner);
|
|
55
|
+
const registrations = [];
|
|
32
56
|
if (hasCatalogs(entity.accessibilityCatalogs)) {
|
|
33
|
-
registrations.push({
|
|
34
|
-
context: itemContext,
|
|
35
|
-
catalogs: entity.accessibilityCatalogs,
|
|
36
|
-
});
|
|
57
|
+
registrations.push({ context, catalogs: entity.accessibilityCatalogs });
|
|
37
58
|
}
|
|
38
59
|
if (hasCatalogs(entity.config?.extractedCatalogs)) {
|
|
39
60
|
registrations.push({
|
|
40
|
-
context
|
|
61
|
+
context,
|
|
41
62
|
catalogs: entity.config.extractedCatalogs,
|
|
42
63
|
});
|
|
43
64
|
}
|
|
65
|
+
if (owner.kind === "passage")
|
|
66
|
+
return registrations;
|
|
44
67
|
for (const model of entity.config?.models ?? []) {
|
|
45
68
|
if (!hasCatalogs(model.accessibilityCatalogs))
|
|
46
69
|
continue;
|
|
47
70
|
registrations.push({
|
|
48
|
-
context: { ...
|
|
71
|
+
context: { ...context, modelId: model.id },
|
|
49
72
|
catalogs: model.accessibilityCatalogs,
|
|
50
73
|
});
|
|
51
74
|
}
|
|
52
75
|
return registrations;
|
|
53
76
|
}
|
|
77
|
+
/** Adapter for the runtime registration event a shell dispatches on mount. */
|
|
78
|
+
export function collectCatalogRegistrations(detail, runtime = {}) {
|
|
79
|
+
return collectEntityCatalogRegistrations(detail.item, {
|
|
80
|
+
kind: detail.kind,
|
|
81
|
+
itemId: detail.itemId,
|
|
82
|
+
canonicalItemId: detail.canonicalItemId,
|
|
83
|
+
assessmentId: runtime.assessmentId,
|
|
84
|
+
sectionId: runtime.sectionId,
|
|
85
|
+
});
|
|
86
|
+
}
|