@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.
Files changed (38) hide show
  1. package/README.md +63 -0
  2. package/dist/components/ItemToolBar.custom-element.js +1 -1
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
  4. package/dist/components/SectionToolBar.custom-element.js +1 -1
  5. package/dist/components/chunks/{ItemToolBar-843902tp.js → ItemToolBar-3cppre9r.js} +26 -26
  6. package/dist/components/chunks/{ItemToolBar-84nv78dy.js → ItemToolBar-7rq2gj8b.js} +1 -1
  7. package/dist/index.d.ts +8 -3
  8. package/dist/index.js +5 -2
  9. package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
  10. package/dist/policy/core/ToolPolicyEngine.js +27 -0
  11. package/dist/policy/core/feature-decision.d.ts +57 -0
  12. package/dist/policy/core/feature-decision.js +40 -0
  13. package/dist/policy/engine.d.ts +1 -0
  14. package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
  15. package/dist/policy/sources/PnpPolicySource.js +41 -11
  16. package/dist/runtime/catalog-registration.d.ts +56 -1
  17. package/dist/runtime/catalog-registration.js +64 -31
  18. package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
  19. package/dist/services/AccessibilityCatalogResolver.js +183 -58
  20. package/dist/services/SSMLExtractor.js +28 -18
  21. package/dist/services/TTSService.d.ts +25 -0
  22. package/dist/services/TTSService.js +241 -45
  23. package/dist/services/ToolkitCoordinator.d.ts +23 -2
  24. package/dist/services/ToolkitCoordinator.js +24 -0
  25. package/dist/services/catalog-media.d.ts +25 -0
  26. package/dist/services/catalog-media.js +101 -0
  27. package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -0
  28. package/dist/services/defaultPersonalNeedsProfile.js +23 -0
  29. package/dist/services/interfaces.d.ts +29 -2
  30. package/dist/services/pnp-standard-features.d.ts +1 -1
  31. package/dist/services/sign-language-cards.d.ts +82 -0
  32. package/dist/services/sign-language-cards.js +133 -0
  33. package/dist/services/spoken-audio-cards.d.ts +54 -0
  34. package/dist/services/spoken-audio-cards.js +66 -0
  35. package/dist/services/tts/math-aware-text-processing.js +3 -3
  36. package/dist/services/tts/text-processing.d.ts +51 -0
  37. package/dist/services/tts/text-processing.js +117 -1
  38. 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,ga as mJ,h as P0,i as CJ,j as $0,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-843902tp.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;
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
+ }
@@ -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
- for (const supportId of allSupports) {
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
- export function collectCatalogRegistrations(detail, runtime = {}) {
3
- const entity = detail.item;
4
- if (!entity)
5
- return [];
6
- const registrations = [];
7
- if (detail.kind === "passage") {
8
- const context = {
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: runtime.assessmentId,
11
- sectionId: runtime.sectionId,
12
- passageId: detail.canonicalItemId || detail.itemId,
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
- const itemContext = {
32
+ return {
26
33
  ownerKind: "itemModel",
27
- assessmentId: runtime.assessmentId,
28
- sectionId: runtime.sectionId,
29
- itemId: detail.itemId,
30
- canonicalItemId: detail.canonicalItemId || detail.itemId,
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: itemContext,
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: { ...itemContext, modelId: model.id },
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
+ }