@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.0

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 (142) hide show
  1. package/README.md +449 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +30 -30
  23. package/dist/policy/core/decision-types.d.ts +44 -32
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +5 -2
  27. package/dist/policy/core/pnp-policy-inputs.js +12 -32
  28. package/dist/policy/core/provenance.d.ts +1 -1
  29. package/dist/policy/core/tool-parameters.d.ts +11 -0
  30. package/dist/policy/core/tool-parameters.js +15 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +14 -22
  33. package/dist/policy/sources/PnpPolicySource.js +30 -28
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +49 -56
  66. package/dist/services/TTSService.js +308 -283
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +82 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +47 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +7 -23
  83. package/dist/services/tool-context.js +99 -53
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +1 -0
  98. package/dist/services/toolbar-decided-tools.js +1 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +4 -20
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +6 -5
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. package/dist/utils/logger.js +0 -10
@@ -50,8 +50,10 @@ export interface ToolPolicyDecisionRequest {
50
50
  /**
51
51
  * Diagnostic codes the engine may emit alongside a decision.
52
52
  * `tool-policy.requiredToolBlocked` fires when a host policy gate
53
- * (`policy.blocked`, missing-from-`placement`, provider veto) removed a tool
54
- * that item or district `requiredTools` mandates.
53
+ * (`policy.blocked`, the allowlist, a provider veto, or placement at no level)
54
+ * removed a tool that item or district `requiredTools` mandates. A tool placed
55
+ * at another level is served there, so its absence from this level's placement
56
+ * is no conflict. `details` is {@link RequiredToolBlockedDetails}.
55
57
  *
56
58
  * `tool-policy.placementMissing` fires when a custom `PolicySource`
57
59
  * references a tool ID that is not present in `tools.placement[level]`
@@ -62,7 +64,9 @@ export interface ToolPolicyDecisionRequest {
62
64
  *
63
65
  * `tool-policy.unknownSupportId` fires for each id a profile, district
64
66
  * policy, test administration or item names that no tool is registered under.
65
- * `details` is {@link UnknownSupportIdDetails}.
67
+ * It describes the inputs, so the engine reports it on its resolved inputs'
68
+ * `diagnostics` rather than on a decision. `details` is
69
+ * {@link UnknownSupportIdDetails}.
66
70
  *
67
71
  * `tool-policy.itemSettingNotApplied` fires on a section-, assessment- or
68
72
  * passage-level decision for each tool on that toolbar a mounted item's
@@ -75,9 +79,10 @@ export interface ToolPolicyDecisionRequest {
75
79
  * `restrictedTools` or the profile's `prohibitedSupports` withdraws instead.
76
80
  * `details` is {@link OverrideBlockedDetails}.
77
81
  *
78
- * The toolkit coordinator logs each diagnostic once per code, tool and item.
82
+ * The toolkit coordinator logs each diagnostic once per code, tool and item,
83
+ * and hands the same ones to its `onPolicyDiagnostic` listeners.
79
84
  */
80
- export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool-policy.placementMissing" | "tool-policy.unknownSupportId" | "tool-policy.itemSettingNotApplied" | "tool-policy.overrideBlocked";
85
+ export type ToolPolicyDiagnosticCode = keyof ToolPolicyDiagnosticDetails;
81
86
  /**
82
87
  * Which host gate removed a profile-mandated tool. Surfaced inside
83
88
  * `ToolPolicyDiagnostic.details` for `tool-policy.requiredToolBlocked`
@@ -87,44 +92,38 @@ export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool
87
92
  * `provenance.features[toolId].allDecisions`.
88
93
  *
89
94
  * The four values mirror steps 1–4 of `composeDecision`:
90
- * - `placement-missing` — tool is not in `tools.placement[level]`
95
+ * - `placement-missing` — no level of `tools.placement` lists the tool
91
96
  * - `provider-disabled` — `tools.providers[id].enabled === false`
92
97
  * - `host-allowlist` — non-empty `tools.policy.allowed` excludes the id
93
98
  * - `host-blocked` — `tools.policy.blocked` lists the id
94
99
  */
95
100
  export type ToolPolicyHostGate = "placement-missing" | "provider-disabled" | "host-allowlist" | "host-blocked";
96
- /**
97
- * Strongly-typed payload for `tool-policy.requiredToolBlocked`
98
- * diagnostics. The diagnostic's `details` field still types as
99
- * `Record<string, unknown>` for forward compatibility, but engines
100
- * always populate this exact shape today and consumers can safely
101
- * cast.
102
- */
103
- export interface RequiredToolBlockedDetails extends Record<string, unknown> {
104
- /** The profile policy rule that mandated the tool (e.g. `district-requirement`). */
105
- rule: string;
101
+ /** Payload of a `tool-policy.requiredToolBlocked` diagnostic. */
102
+ export interface RequiredToolBlockedDetails {
103
+ /** The requirement that mandates the tool. */
104
+ rule: "item-requirement" | "district-requirement";
106
105
  /** Which host gate removed the tool. */
107
106
  hostRule: ToolPolicyHostGate;
108
107
  /** The host configuration value that triggered the gate (best-effort, may be omitted). */
109
108
  hostValue?: unknown;
110
109
  }
111
110
  /** Payload of a `tool-policy.unknownSupportId` diagnostic. */
112
- export interface UnknownSupportIdDetails extends Record<string, unknown> {
111
+ export interface UnknownSupportIdDetails {
113
112
  /** The rules whose lists name the id, in precedence order. */
114
113
  origins: PnpPolicySourceRule[];
115
114
  }
116
115
  /** Payload of a `tool-policy.placementMissing` diagnostic. */
117
- export interface PlacementMissingDetails extends Record<string, unknown> {
116
+ export interface PlacementMissingDetails {
118
117
  /** The custom `PolicySource` that named the tool. */
119
118
  customSourceId: string;
120
119
  }
121
120
  /** Payload of a `tool-policy.overrideBlocked` diagnostic. */
122
- export interface OverrideBlockedDetails extends Record<string, unknown> {
121
+ export interface OverrideBlockedDetails {
123
122
  /** The rule that withdrew the granted tool. */
124
123
  rule: "item-restriction" | "pnp-prohibited";
125
124
  }
126
125
  /** Payload of a `tool-policy.itemSettingNotApplied` diagnostic. */
127
- export interface ItemSettingNotAppliedDetails extends Record<string, unknown> {
126
+ export interface ItemSettingNotAppliedDetails {
128
127
  /** Canonical id of the item whose setting names the tool. */
129
128
  itemId: string;
130
129
  /** The item settings that name it. */
@@ -132,14 +131,28 @@ export interface ItemSettingNotAppliedDetails extends Record<string, unknown> {
132
131
  /** The level of the toolbar the tool is on. */
133
132
  toolbarLevel: ToolLevel;
134
133
  }
135
- export interface ToolPolicyDiagnostic {
136
- code: ToolPolicyDiagnosticCode;
137
- /** The toolbar level decided; absent on a feature decision. */
138
- level?: ToolPlacementLevel;
139
- toolId: string;
140
- message: string;
141
- details?: Record<string, unknown>;
134
+ /** The payload each diagnostic code carries. */
135
+ export interface ToolPolicyDiagnosticDetails {
136
+ "tool-policy.requiredToolBlocked": RequiredToolBlockedDetails;
137
+ "tool-policy.placementMissing": PlacementMissingDetails;
138
+ "tool-policy.unknownSupportId": UnknownSupportIdDetails;
139
+ "tool-policy.itemSettingNotApplied": ItemSettingNotAppliedDetails;
140
+ "tool-policy.overrideBlocked": OverrideBlockedDetails;
142
141
  }
142
+ /** A policy diagnostic, its `details` typed by its `code`. */
143
+ export type ToolPolicyDiagnostic = {
144
+ [C in ToolPolicyDiagnosticCode]: {
145
+ code: C;
146
+ /**
147
+ * The toolbar level decided; absent on a feature decision and on an input
148
+ * diagnostic.
149
+ */
150
+ level?: ToolPlacementLevel;
151
+ toolId: string;
152
+ message: string;
153
+ details: ToolPolicyDiagnosticDetails[C];
154
+ };
155
+ }[ToolPolicyDiagnosticCode];
143
156
  export interface ToolPolicyEntry {
144
157
  toolId: string;
145
158
  /**
@@ -158,12 +171,11 @@ export interface ToolPolicyEntry {
158
171
  */
159
172
  alwaysAvailable: boolean;
160
173
  /**
161
- * Tool-specific settings resolved from the assessment / item
162
- * settings via the PNP policy source. Hosts that want to read these
163
- * directly should use `entry.settings` rather than peeking into
164
- * the assessment entity from above the engine.
174
+ * The tool's parameters: the item's `toolParameters` entry on the item's own
175
+ * toolbar, else the assessment's `toolParameters` entry. Set whether or not a
176
+ * grant admitted the tool.
165
177
  */
166
- settings?: unknown;
178
+ parameters?: Record<string, unknown>;
167
179
  }
168
180
  export interface ToolPolicyDecision {
169
181
  visibleTools: ToolPolicyEntry[];
@@ -27,10 +27,10 @@ import type { PnpPolicySourceRule, ToolPolicyResolutionDecision, ToolPolicySourc
27
27
  * A feature verdict comes from one of the eight PNP precedence levels, or from a
28
28
  * host gate that never reaches them: `tools.policy.blocked` and a non-empty
29
29
  * `tools.policy.allowed` are absolute for the id they name, exactly as they are
30
- * on the placement-scoped path.
30
+ * on the placement-scoped path. `"none"` is a feature no rule names.
31
31
  */
32
- export type FeaturePolicyRule = PnpPolicySourceRule | "host-allowlist" | "host-blocked";
33
- export interface FeaturePolicyDecision {
32
+ export type FeaturePolicyRule = PnpPolicySourceRule | "host-allowlist" | "host-blocked" | "none";
33
+ export interface FeaturePolicyDecision<P = Record<string, unknown>> {
34
34
  /** The PNP/AfA support id that was evaluated (e.g. `"signLanguage"`). */
35
35
  featureId: string;
36
36
  /**
@@ -42,7 +42,8 @@ export interface FeaturePolicyDecision {
42
42
  action: ToolPolicyResolutionDecision["action"];
43
43
  /** Which precedence rule produced the verdict. */
44
44
  rule: FeaturePolicyRule;
45
- precedence: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
45
+ /** The rule's precedence level; `null` for `rule: "none"`. */
46
+ precedence: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null;
46
47
  sourceType: ToolPolicySourceType;
47
48
  /** Human-readable explanation, suitable for a policy debugger. */
48
49
  reason: string;
@@ -66,20 +67,21 @@ export interface FeaturePolicyDecision {
66
67
  */
67
68
  assessmentBound: boolean;
68
69
  /**
69
- * `true` when the grant is a mandate (item or district `requiredTools`)
70
- * rather than a student-profile support or a test-administration override.
70
+ * `true` when item or district `requiredTools` mandates the feature, including
71
+ * under a `true` test-administration override.
71
72
  */
72
73
  required: boolean;
73
74
  /**
74
- * Feature parameters resolved from item `toolParameters` then assessment
75
- * `toolConfigs`, keyed by the feature id. The seam a later configurable
76
- * presentation would hang on; no vocabulary is defined yet.
75
+ * The feature's parameters: the item's `toolParameters` entry for an item's
76
+ * scope, else the assessment's `toolParameters` entry, keyed by the feature id.
77
+ * Set on a denial too; a host denial leaves it out.
77
78
  */
78
- parameters?: unknown;
79
+ parameters?: P;
79
80
  /**
80
- * Conflicts found while deciding: a `tool-policy.unknownSupportId` for each
81
- * id the bound inputs name that no tool is registered under. Empty on a host
82
- * denial, which consults no policy source.
81
+ * Conflicts found while deciding: a `tool-policy.overrideBlocked` for a
82
+ * granting override the decision withdrew. Empty on a host denial, which
83
+ * consults no policy source. An id no tool is registered under describes the
84
+ * inputs, so it is reported on the engine inputs' `diagnostics`.
83
85
  */
84
86
  diagnostics: ToolPolicyDiagnostic[];
85
87
  }
@@ -97,7 +99,7 @@ export interface FeatureDecisionContext {
97
99
  * blocklist is a verdict whether or not an assessment was bound, and the flag
98
100
  * only ever qualifies a *policy* denial.
99
101
  */
100
- export declare function hostFeatureDenial(featureId: string, rule: "host-allowlist" | "host-blocked", hostValue: readonly string[], context: FeatureDecisionContext): FeaturePolicyDecision;
102
+ export declare function hostFeatureDenial(featureId: string, rule: "host-allowlist" | "host-blocked", hostValue: readonly string[], context: FeatureDecisionContext): FeaturePolicyDecision<never>;
101
103
  /**
102
104
  * Whether a host gate produced the verdict, rather than a policy source.
103
105
  *
@@ -114,4 +116,4 @@ export declare function isHostDeniedFeature(decision: Pick<FeaturePolicyDecision
114
116
  * exactly one decision and at most one flags entry — no mapped-tool-id
115
117
  * bookkeeping is needed to read it back out.
116
118
  */
117
- export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult, context: FeatureDecisionContext): FeaturePolicyDecision;
119
+ export declare function interpretFeatureResult<P>(featureId: string, result: PnpPolicyResult, context: FeatureDecisionContext, parameters: P | undefined): FeaturePolicyDecision<P>;
@@ -20,14 +20,12 @@
20
20
  * "nothing was bound to ask against" are the same verdict for different
21
21
  * reasons, and only the second is a defect.
22
22
  */
23
- import { overrideBlockedDiagnostic, unknownSupportIdDiagnostic, } from "./compose-decision.js";
23
+ import { overrideBlockedDiagnostic } from "./compose-decision.js";
24
24
  /**
25
25
  * Reason text for a denial that had no assessment to decide against.
26
26
  *
27
- * Used in place of the `pnp-support` skip's "not configured at any level",
28
- * which is true but reads as a completed evaluation. `rule` and `precedence`
29
- * stay as the source reported them: nothing fired, so naming a ninth rule
30
- * would describe a precedence level that does not exist.
27
+ * Used in place of "not configured", which is true but reads as a completed
28
+ * evaluation.
31
29
  */
32
30
  const unboundAssessmentReason = (featureId) => `No assessment is bound, so no policy source could grant "${featureId}"`;
33
31
  /**
@@ -73,17 +71,20 @@ export function isHostDeniedFeature(decision) {
73
71
  * exactly one decision and at most one flags entry — no mapped-tool-id
74
72
  * bookkeeping is needed to read it back out.
75
73
  */
76
- export function interpretFeatureResult(featureId, result, context) {
74
+ export function interpretFeatureResult(featureId, result, context, parameters) {
77
75
  const decision = result.decisions[0];
78
76
  const flags = Array.from(result.perToolFlags.values())[0];
79
77
  const granted = decision?.action === "enable";
80
78
  const reason = decision?.reason ?? `Feature "${featureId}" not configured`;
79
+ // The source logs "no rule fired" as a skip at the last level so the trail
80
+ // stays ordered; the verdict names no rule for it.
81
+ const fired = decision !== undefined && decision.action !== "skip";
81
82
  return {
82
83
  featureId,
83
84
  granted,
84
85
  action: decision?.action ?? "skip",
85
- rule: decision?.rule ?? "pnp-support",
86
- precedence: decision?.precedence ?? 8,
86
+ rule: fired ? decision.rule : "none",
87
+ precedence: fired ? decision.precedence : null,
87
88
  sourceType: decision?.sourceType ?? "system",
88
89
  // Only a denial is re-worded. An unbound host can still be granted the
89
90
  // feature — an item's registered settings carrying `requiredTools` mandate
@@ -93,11 +94,8 @@ export function interpretFeatureResult(featureId, result, context) {
93
94
  ? reason
94
95
  : unboundAssessmentReason(featureId),
95
96
  required: Boolean(flags?.required),
96
- parameters: flags?.settings,
97
+ parameters,
97
98
  assessmentBound: context.assessmentBound,
98
- diagnostics: [
99
- ...Array.from(result.unmappedSupportIds, ([supportId, origins]) => unknownSupportIdDiagnostic(supportId, origins)),
100
- ...Array.from(result.blockedOverrides, ([toolId, rule]) => overrideBlockedDiagnostic(toolId, rule)),
101
- ],
99
+ diagnostics: Array.from(result.blockedOverrides, ([toolId, rule]) => overrideBlockedDiagnostic(toolId, rule)),
102
100
  };
103
101
  }
@@ -15,7 +15,10 @@
15
15
  * - `settings.districtPolicy` (any `blockedTools` or `requiredTools`),
16
16
  * - `settings.testAdministration` (any `toolOverrides` entry),
17
17
  * - or, for an item-scoped decision, the item's settings carry
18
- * `requiredTools` / `restrictedTools` / `toolParameters`.
18
+ * `requiredTools` / `restrictedTools`.
19
+ *
20
+ * Tool parameters are no policy material: the engine resolves them whatever the
21
+ * enforcement state.
19
22
  *
20
23
  * Hosts opt out of the auto-on behavior by passing
21
24
  * `pnpEnforcement: "off"` explicitly (engine input or
@@ -32,7 +35,7 @@ import type { AssessmentEntity, ItemSettings } from "@pie-players/pie-players-sh
32
35
  export declare function assessmentHasPnpPolicyInputs(assessment: AssessmentEntity | null | undefined): boolean;
33
36
  /**
34
37
  * Return `true` when an item's settings carry item-level profile policy
35
- * material (`requiredTools`, `restrictedTools`, or `toolParameters`).
38
+ * material (`requiredTools` or `restrictedTools`).
36
39
  *
37
40
  * Used in addition to {@link assessmentHasPnpPolicyInputs} for a decision scoped
38
41
  * to the item, so an item with profile-relevant settings engages PNP/profile
@@ -15,12 +15,16 @@
15
15
  * - `settings.districtPolicy` (any `blockedTools` or `requiredTools`),
16
16
  * - `settings.testAdministration` (any `toolOverrides` entry),
17
17
  * - or, for an item-scoped decision, the item's settings carry
18
- * `requiredTools` / `restrictedTools` / `toolParameters`.
18
+ * `requiredTools` / `restrictedTools`.
19
+ *
20
+ * Tool parameters are no policy material: the engine resolves them whatever the
21
+ * enforcement state.
19
22
  *
20
23
  * Hosts opt out of the auto-on behavior by passing
21
24
  * `pnpEnforcement: "off"` explicitly (engine input or
22
25
  * `ToolkitCoordinator.setPnpEnforcement("off")`).
23
26
  */
27
+ const hasEntries = (value) => Array.isArray(value) && value.length > 0;
24
28
  /**
25
29
  * Return `true` when the assessment carries any PNP/profile policy
26
30
  * material that the engine's `PnpPolicySource` would consume.
@@ -32,26 +36,15 @@ export function assessmentHasPnpPolicyInputs(assessment) {
32
36
  if (!assessment)
33
37
  return false;
34
38
  const pnp = assessment.personalNeedsProfile;
35
- if (pnp) {
36
- if (Array.isArray(pnp.supports) && pnp.supports.length > 0)
37
- return true;
38
- if (Array.isArray(pnp.prohibitedSupports) &&
39
- pnp.prohibitedSupports.length > 0) {
40
- return true;
41
- }
39
+ if (pnp && (hasEntries(pnp.supports) || hasEntries(pnp.prohibitedSupports))) {
40
+ return true;
42
41
  }
43
42
  const settings = assessment.settings;
44
43
  if (settings) {
45
44
  const district = settings.districtPolicy;
46
- if (district) {
47
- if (Array.isArray(district.blockedTools) &&
48
- district.blockedTools.length > 0) {
49
- return true;
50
- }
51
- if (Array.isArray(district.requiredTools) &&
52
- district.requiredTools.length > 0) {
53
- return true;
54
- }
45
+ if (district &&
46
+ (hasEntries(district.blockedTools) || hasEntries(district.requiredTools))) {
47
+ return true;
55
48
  }
56
49
  const overrides = settings.testAdministration?.toolOverrides;
57
50
  if (overrides &&
@@ -64,7 +57,7 @@ export function assessmentHasPnpPolicyInputs(assessment) {
64
57
  }
65
58
  /**
66
59
  * Return `true` when an item's settings carry item-level profile policy
67
- * material (`requiredTools`, `restrictedTools`, or `toolParameters`).
60
+ * material (`requiredTools` or `restrictedTools`).
68
61
  *
69
62
  * Used in addition to {@link assessmentHasPnpPolicyInputs} for a decision scoped
70
63
  * to the item, so an item with profile-relevant settings engages PNP/profile
@@ -74,20 +67,7 @@ export function assessmentHasPnpPolicyInputs(assessment) {
74
67
  export function itemSettingsHavePnpPolicyInputs(settings) {
75
68
  if (!settings)
76
69
  return false;
77
- if (Array.isArray(settings.requiredTools) &&
78
- settings.requiredTools.length > 0) {
79
- return true;
80
- }
81
- if (Array.isArray(settings.restrictedTools) &&
82
- settings.restrictedTools.length > 0) {
83
- return true;
84
- }
85
- if (settings.toolParameters &&
86
- typeof settings.toolParameters === "object" &&
87
- Object.keys(settings.toolParameters).length > 0) {
88
- return true;
89
- }
90
- return false;
70
+ return hasEntries(settings.requiredTools) || hasEntries(settings.restrictedTools);
91
71
  }
92
72
  /**
93
73
  * Resolve the auto-mode `pnpEnforcement` for one decision.
@@ -20,7 +20,7 @@ export interface ToolPolicyResolutionDecision {
20
20
  /** Monotonic step index across the entire decision log. */
21
21
  step: number;
22
22
  /**
23
- * Precedence level. PNP/profile rules use `1`–`6`; non-profile rules use `0`
23
+ * Precedence level. PNP/profile rules use `1`–`8`; non-profile rules use `0`
24
24
  * for host-side gates, and a custom source, which runs after PNP/profile
25
25
  * policy, uses its own or `100 + i` for the `i`th registered source.
26
26
  */
@@ -0,0 +1,11 @@
1
+ import type { AssessmentEntity, ToolParametersFor } from "@pie-players/pie-players-shared/types";
2
+ import type { PnpPolicyItem } from "../sources/PnpPolicySource.js";
3
+ /**
4
+ * A tool's parameters for one decision: the item's `toolParameters` entry, else
5
+ * the assessment's `toolParameters` entry.
6
+ *
7
+ * Parameters say how a tool behaves wherever policy shows it, so they reach a
8
+ * tool placed without a grant, and PNP enforcement does not gate them. `item` is
9
+ * set only for a decision about the item's own toolbar or content.
10
+ */
11
+ export declare function resolveToolParameters<K extends string>(toolId: K, assessment: AssessmentEntity | null | undefined, item: PnpPolicyItem | undefined): ToolParametersFor<K> | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A tool's parameters for one decision: the item's `toolParameters` entry, else
3
+ * the assessment's `toolParameters` entry.
4
+ *
5
+ * Parameters say how a tool behaves wherever policy shows it, so they reach a
6
+ * tool placed without a grant, and PNP enforcement does not gate them. `item` is
7
+ * set only for a decision about the item's own toolbar or content.
8
+ */
9
+ export function resolveToolParameters(toolId, assessment, item) {
10
+ const settings = assessment?.settings;
11
+ const value = item?.settings.toolParameters?.[toolId] ?? settings?.toolParameters?.[toolId];
12
+ return value && typeof value === "object" && !Array.isArray(value)
13
+ ? value
14
+ : undefined;
15
+ }
@@ -7,6 +7,6 @@
7
7
  */
8
8
  export { ToolPolicyEngine, type PnpEnforcementMode, type ResolvedEngineInputs, type ToolPolicyChangeEvent, type ToolPolicyChangeListener, type ToolPolicyEngineArgs, type ToolPolicyEngineInputs, } from "./core/ToolPolicyEngine.js";
9
9
  export { isHostDeniedFeature, type FeaturePolicyDecision, type FeaturePolicyRule, } from "./core/feature-decision.js";
10
- export type { ItemSettingNotAppliedDetails, OverrideBlockedDetails, PlacementMissingDetails, RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, UnknownSupportIdDetails, } from "./core/decision-types.js";
10
+ export type { ItemSettingNotAppliedDetails, OverrideBlockedDetails, PlacementMissingDetails, RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyDiagnosticDetails, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, UnknownSupportIdDetails, } from "./core/decision-types.js";
11
11
  export type { PolicySource, PolicySourceDecisionContext, PolicySourceProvenanceEntry, PolicySourceResult, } from "./core/PolicySource.js";
12
12
  export type { PnpPolicySourceRule, ToolPolicyDecisionRule, ToolPolicyFeatureTrail, ToolPolicyProvenance, ToolPolicyResolutionDecision, ToolPolicySourceType, } from "./core/provenance.js";
@@ -20,8 +20,6 @@ export interface PnpPolicyToolFlags {
20
20
  * override grants it (host UI cannot toggle off).
21
21
  */
22
22
  alwaysAvailable: boolean;
23
- /** Tool-specific settings derived from item / assessment settings. */
24
- settings?: unknown;
25
23
  /** Which PNP/profile rule contributed the surviving verdict. */
26
24
  rule: PnpPolicySourceRule;
27
25
  }
@@ -60,11 +58,12 @@ export interface PnpPolicyResult {
60
58
  */
61
59
  blockedToolIds: Set<string>;
62
60
  /**
63
- * Tool IDs PNP/profile policy mandates (item or district `requiredTools`). Used
64
- * by the engine to detect `tool-policy.requiredToolBlocked`
61
+ * Tools PNP/profile policy mandates (item or district `requiredTools`), each
62
+ * with the requirement that names it, including a tool a `true` override
63
+ * decides. Used by the engine to detect `tool-policy.requiredToolBlocked`
65
64
  * diagnostics for tools removed by host policy.
66
65
  */
67
- mandatedToolIds: Set<string>;
66
+ mandates: Map<string, "item-requirement" | "district-requirement">;
68
67
  /**
69
68
  * Per-tool flags merged into surviving `ToolPolicyEntry`s.
70
69
  */
@@ -75,15 +74,6 @@ export interface PnpPolicyResult {
75
74
  * support id).
76
75
  */
77
76
  decisions: PnpPolicyDecisionEvent[];
78
- /**
79
- * Ids named by a profile, district policy, test administration or item that
80
- * no tool is registered under, each with the rules whose lists name it, in
81
- * precedence order. Nothing resolves them; the engine turns each into a
82
- * `tool-policy.unknownSupportId` diagnostic. Empty when the registry is: an
83
- * empty registry has nothing to check an id against, and
84
- * `tool-config-validation` already reports it once.
85
- */
86
- unmappedSupportIds: Map<string, PnpPolicySourceRule[]>;
87
77
  /**
88
78
  * Ids a `true` test-administration override names that an item restriction
89
79
  * or a PNP prohibition withdrew instead, each with the rule that did. The
@@ -125,8 +115,8 @@ export declare class PnpPolicySource {
125
115
  * feature: the returned result carries exactly one decision, and
126
116
  * `decisions[0].action` is the verdict.
127
117
  *
128
- * Reusing `resolveSupport(...)` rather than re-walking the precedence rules
129
- * is the point — a second copy of the eight levels would drift.
118
+ * It reuses `resolveSupport(...)` because a second copy of the eight levels
119
+ * would drift.
130
120
  */
131
121
  resolveFeature(featureId: string, args: PnpPolicyApplyArgs): PnpPolicyResult;
132
122
  /**
@@ -136,11 +126,13 @@ export declare class PnpPolicySource {
136
126
  private prepare;
137
127
  private resolveSupport;
138
128
  /**
139
- * The named ids no tool is registered under, with the rules naming each in
140
- * precedence order. A support id is the id of the tool it grants, so an id
141
- * missing from the registry matches nothing placed.
129
+ * Ids the assessment or any of `items` names that no tool is registered
130
+ * under, each with the rules whose lists name it, in precedence order. A
131
+ * support id is the id of the tool it grants, so an id missing from the
132
+ * registry matches nothing placed; the engine turns each into a
133
+ * `tool-policy.unknownSupportId` diagnostic. Empty when the registry is: an
134
+ * empty registry has nothing to check an id against, and
135
+ * `tool-config-validation` already reports it once.
142
136
  */
143
- private unmappedIds;
144
- /** Tool parameters: the item's entry, else the assessment's. */
145
- private resolveToolSettings;
137
+ unknownSupportIds(assessment: AssessmentEntity | null | undefined, items: readonly ItemSettings[]): Map<string, PnpPolicySourceRule[]>;
146
138
  }
@@ -42,8 +42,8 @@ export class PnpPolicySource {
42
42
  * feature: the returned result carries exactly one decision, and
43
43
  * `decisions[0].action` is the verdict.
44
44
  *
45
- * Reusing `resolveSupport(...)` rather than re-walking the precedence rules
46
- * is the point — a second copy of the eight levels would drift.
45
+ * It reuses `resolveSupport(...)` because a second copy of the eight levels
46
+ * would drift.
47
47
  */
48
48
  resolveFeature(featureId, args) {
49
49
  const { ctx, result } = this.prepare(args);
@@ -61,10 +61,9 @@ export class PnpPolicySource {
61
61
  const itemSettings = item?.settings;
62
62
  const result = {
63
63
  blockedToolIds: new Set(),
64
- mandatedToolIds: new Set(),
64
+ mandates: new Map(),
65
65
  perToolFlags: new Map(),
66
66
  decisions: [],
67
- unmappedSupportIds: this.unmappedIds(pnp, settings, itemSettings),
68
67
  blockedOverrides: new Map(),
69
68
  sources: {},
70
69
  };
@@ -94,7 +93,6 @@ export class PnpPolicySource {
94
93
  districtPolicy: settings?.districtPolicy,
95
94
  testAdmin: settings?.testAdministration,
96
95
  itemSettings,
97
- toolConfigs: settings?.toolConfigs,
98
96
  };
99
97
  return { ctx, result };
100
98
  }
@@ -164,13 +162,19 @@ export class PnpPolicySource {
164
162
  });
165
163
  return;
166
164
  }
165
+ const mandate = ctx.itemSettings?.requiredTools?.includes(supportId)
166
+ ? "item-requirement"
167
+ : ctx.districtPolicy?.requiredTools?.includes(supportId)
168
+ ? "district-requirement"
169
+ : undefined;
167
170
  // 5. Test administration override `true`: grants the support for the
168
- // session.
171
+ // session. A requirement naming it keeps its mandate.
169
172
  if (override === true) {
173
+ if (mandate)
174
+ out.mandates.set(supportId, mandate);
170
175
  out.perToolFlags.set(supportId, {
171
- required: false,
176
+ required: mandate !== undefined,
172
177
  alwaysAvailable: true,
173
- settings: this.resolveToolSettings(supportId, ctx),
174
178
  rule: "test-admin-override",
175
179
  });
176
180
  out.decisions.push({
@@ -185,12 +189,11 @@ export class PnpPolicySource {
185
189
  return;
186
190
  }
187
191
  // 6. Item requirement (forces enable)
188
- if (ctx.itemSettings?.requiredTools?.includes(supportId)) {
189
- out.mandatedToolIds.add(supportId);
192
+ if (mandate === "item-requirement") {
193
+ out.mandates.set(supportId, mandate);
190
194
  out.perToolFlags.set(supportId, {
191
195
  required: true,
192
196
  alwaysAvailable: false,
193
- settings: this.resolveToolSettings(supportId, ctx),
194
197
  rule: "item-requirement",
195
198
  });
196
199
  out.decisions.push({
@@ -200,17 +203,16 @@ export class PnpPolicySource {
200
203
  action: "enable",
201
204
  sourceType: "item",
202
205
  reason: `Item requires "${supportId}" for this question`,
203
- value: ctx.itemSettings.requiredTools,
206
+ value: ctx.itemSettings?.requiredTools,
204
207
  });
205
208
  return;
206
209
  }
207
210
  // 7. District requirement
208
- if (ctx.districtPolicy?.requiredTools?.includes(supportId)) {
209
- out.mandatedToolIds.add(supportId);
211
+ if (mandate === "district-requirement") {
212
+ out.mandates.set(supportId, mandate);
210
213
  out.perToolFlags.set(supportId, {
211
214
  required: true,
212
215
  alwaysAvailable: false,
213
- settings: this.resolveToolSettings(supportId, ctx),
214
216
  rule: "district-requirement",
215
217
  });
216
218
  out.decisions.push({
@@ -220,7 +222,7 @@ export class PnpPolicySource {
220
222
  action: "enable",
221
223
  sourceType: "assessment",
222
224
  reason: `District policy requires "${supportId}" for all assessments`,
223
- value: ctx.districtPolicy.requiredTools,
225
+ value: ctx.districtPolicy?.requiredTools,
224
226
  });
225
227
  return;
226
228
  }
@@ -229,7 +231,6 @@ export class PnpPolicySource {
229
231
  out.perToolFlags.set(supportId, {
230
232
  required: false,
231
233
  alwaysAvailable: true,
232
- settings: this.resolveToolSettings(supportId, ctx),
233
234
  rule: "pnp-support",
234
235
  });
235
236
  out.decisions.push({
@@ -256,23 +257,29 @@ export class PnpPolicySource {
256
257
  });
257
258
  }
258
259
  /**
259
- * The named ids no tool is registered under, with the rules naming each in
260
- * precedence order. A support id is the id of the tool it grants, so an id
261
- * missing from the registry matches nothing placed.
260
+ * Ids the assessment or any of `items` names that no tool is registered
261
+ * under, each with the rules whose lists name it, in precedence order. A
262
+ * support id is the id of the tool it grants, so an id missing from the
263
+ * registry matches nothing placed; the engine turns each into a
264
+ * `tool-policy.unknownSupportId` diagnostic. Empty when the registry is: an
265
+ * empty registry has nothing to check an id against, and
266
+ * `tool-config-validation` already reports it once.
262
267
  */
263
- unmappedIds(pnp, settings, itemSettings) {
268
+ unknownSupportIds(assessment, items) {
264
269
  const unmapped = new Map();
265
270
  if (this.toolRegistry.getAllTools().length === 0)
266
271
  return unmapped;
272
+ const pnp = assessment?.personalNeedsProfile;
273
+ const settings = assessment?.settings;
267
274
  const named = [
268
275
  ["district-block", settings?.districtPolicy?.blockedTools],
269
276
  [
270
277
  "test-admin-override",
271
278
  Object.keys(settings?.testAdministration?.toolOverrides ?? {}),
272
279
  ],
273
- ["item-restriction", itemSettings?.restrictedTools],
280
+ ["item-restriction", items.flatMap((item) => item.restrictedTools ?? [])],
274
281
  ["pnp-prohibited", pnp?.prohibitedSupports],
275
- ["item-requirement", itemSettings?.requiredTools],
282
+ ["item-requirement", items.flatMap((item) => item.requiredTools ?? [])],
276
283
  ["district-requirement", settings?.districtPolicy?.requiredTools],
277
284
  ["pnp-support", pnp?.supports],
278
285
  ];
@@ -288,9 +295,4 @@ export class PnpPolicySource {
288
295
  }
289
296
  return unmapped;
290
297
  }
291
- /** Tool parameters: the item's entry, else the assessment's. */
292
- resolveToolSettings(supportId, ctx) {
293
- return (ctx.itemSettings?.toolParameters?.[supportId] ??
294
- ctx.toolConfigs?.[supportId]);
295
- }
296
298
  }