@pie-players/pie-assessment-toolkit 0.3.74 → 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 (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -1,6 +1,5 @@
1
1
  /**
2
- * PNP Policy Source for the M8 engine
3
- * (see
2
+ * PNP Policy Source: step 5 of `composeDecision` (see
4
3
  * `docs/tools-and-accomodations/architecture.md`).
5
4
  *
6
5
  * This source applies the PNP/profile precedence rules as a `(candidates, pnpPolicyInputs) →
@@ -16,12 +15,15 @@ export class PnpPolicySource {
16
15
  }
17
16
  apply(args) {
18
17
  const { ctx, result } = this.prepare(args);
19
- const { pnp, districtPolicy, itemSettings } = ctx;
18
+ const { pnp, districtPolicy, testAdmin, itemSettings } = ctx;
20
19
  const allSupports = new Set();
21
20
  pnp?.supports?.forEach((s) => allSupports.add(s));
22
21
  pnp?.prohibitedSupports?.forEach((s) => allSupports.add(s));
23
22
  districtPolicy?.blockedTools?.forEach((s) => allSupports.add(s));
24
23
  districtPolicy?.requiredTools?.forEach((s) => allSupports.add(s));
24
+ for (const s of Object.keys(testAdmin?.toolOverrides ?? {})) {
25
+ allSupports.add(s);
26
+ }
25
27
  itemSettings?.requiredTools?.forEach((s) => allSupports.add(s));
26
28
  itemSettings?.restrictedTools?.forEach((s) => allSupports.add(s));
27
29
  for (const supportId of allSupports) {
@@ -30,20 +32,18 @@ export class PnpPolicySource {
30
32
  return result;
31
33
  }
32
34
  /**
33
- * Evaluate exactly one PNP support id through the same six-level
35
+ * Evaluate exactly one PNP support id through the same eight-level
34
36
  * precedence `apply(...)` uses.
35
37
  *
36
38
  * This exists for **policy-addressable capabilities that are not toolbar
37
39
  * tools** — a signed alternate rendered as its own region, for example.
38
40
  * `apply(...)` only evaluates support ids that appear somewhere in the
39
- * bound policy inputs, and it keys its result maps by *mapped tool id*, so
40
- * a caller asking about one feature would have to re-derive that mapping
41
- * and could collide with another support id that maps to the same tool.
42
- * Evaluating one id in isolation avoids both problems: the returned result
43
- * carries exactly one decision, and `decisions[0].action` is the verdict.
41
+ * bound policy inputs. Evaluating one id in isolation answers for any
42
+ * feature: the returned result carries exactly one decision, and
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 six 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);
@@ -55,16 +55,16 @@ export class PnpPolicySource {
55
55
  * configuration-source attribution) from the bound policy inputs.
56
56
  */
57
57
  prepare(args) {
58
- const { assessment, currentItemRef } = args;
58
+ const { assessment, item } = args;
59
59
  const pnp = assessment?.personalNeedsProfile;
60
60
  const settings = assessment?.settings;
61
- const itemSettings = currentItemRef?.settings;
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: new Set(),
67
+ blockedOverrides: new Map(),
68
68
  sources: {},
69
69
  };
70
70
  if (settings?.districtPolicy || settings?.testAdministration) {
@@ -81,11 +81,11 @@ export class PnpPolicySource {
81
81
  config: pnp,
82
82
  };
83
83
  }
84
- if (currentItemRef && itemSettings) {
84
+ if (item) {
85
85
  result.sources.item = {
86
- id: currentItemRef.identifier,
87
- name: currentItemRef.identifier,
88
- config: itemSettings,
86
+ id: item.id,
87
+ name: item.id,
88
+ config: item.settings,
89
89
  };
90
90
  }
91
91
  const ctx = {
@@ -93,19 +93,17 @@ export class PnpPolicySource {
93
93
  districtPolicy: settings?.districtPolicy,
94
94
  testAdmin: settings?.testAdministration,
95
95
  itemSettings,
96
- toolConfigs: settings?.toolConfigs,
97
96
  };
98
97
  return { ctx, result };
99
98
  }
100
99
  resolveSupport(supportId, ctx, out) {
101
100
  // 1. District block (absolute veto)
102
101
  if (ctx.districtPolicy?.blockedTools?.includes(supportId)) {
103
- const toolId = this.mapSupportToToolId(supportId, out);
104
- out.blockedToolIds.add(toolId);
102
+ out.blockedToolIds.add(supportId);
105
103
  out.decisions.push({
106
104
  precedence: 1,
107
105
  rule: "district-block",
108
- featureId: toolId,
106
+ featureId: supportId,
109
107
  action: "block",
110
108
  sourceType: "assessment",
111
109
  reason: `District policy blocks "${supportId}" for all assessments`,
@@ -113,29 +111,32 @@ export class PnpPolicySource {
113
111
  });
114
112
  return;
115
113
  }
116
- // 2. Test administration override
117
- if (ctx.testAdmin?.toolOverrides?.[supportId] === false) {
118
- const toolId = this.mapSupportToToolId(supportId, out);
119
- out.blockedToolIds.add(toolId);
114
+ // 2. Test administration override `false`: withdraws the support for the
115
+ // session, outranking every level below.
116
+ const override = ctx.testAdmin?.toolOverrides?.[supportId];
117
+ if (override === false) {
118
+ out.blockedToolIds.add(supportId);
120
119
  out.decisions.push({
121
120
  precedence: 2,
122
121
  rule: "test-admin-override",
123
- featureId: toolId,
122
+ featureId: supportId,
124
123
  action: "block",
125
124
  sourceType: "assessment",
126
125
  reason: `Test administrator disabled "${supportId}" for this session`,
127
- value: ctx.testAdmin.toolOverrides,
126
+ value: ctx.testAdmin?.toolOverrides,
128
127
  });
129
128
  return;
130
129
  }
131
130
  // 3. Item restriction (per-item block)
132
131
  if (ctx.itemSettings?.restrictedTools?.includes(supportId)) {
133
- const toolId = this.mapSupportToToolId(supportId, out);
134
- out.blockedToolIds.add(toolId);
132
+ out.blockedToolIds.add(supportId);
133
+ if (override === true) {
134
+ out.blockedOverrides.set(supportId, "item-restriction");
135
+ }
135
136
  out.decisions.push({
136
137
  precedence: 3,
137
138
  rule: "item-restriction",
138
- featureId: toolId,
139
+ featureId: supportId,
139
140
  action: "block",
140
141
  sourceType: "item",
141
142
  reason: `Item restricts "${supportId}" (e.g., mental math question blocks calculator)`,
@@ -143,78 +144,99 @@ export class PnpPolicySource {
143
144
  });
144
145
  return;
145
146
  }
146
- // 4. Item requirement (forces enable)
147
- if (ctx.itemSettings?.requiredTools?.includes(supportId)) {
148
- const toolId = this.mapSupportToToolId(supportId, out);
149
- out.mandatedToolIds.add(toolId);
150
- out.perToolFlags.set(toolId, {
147
+ // 4. Profile prohibition: the student's profile withdraws the support,
148
+ // outranking a `true` override and every requirement.
149
+ if (ctx.pnp?.prohibitedSupports?.includes(supportId)) {
150
+ out.blockedToolIds.add(supportId);
151
+ if (override === true) {
152
+ out.blockedOverrides.set(supportId, "pnp-prohibited");
153
+ }
154
+ out.decisions.push({
155
+ precedence: 4,
156
+ rule: "pnp-prohibited",
157
+ featureId: supportId,
158
+ action: "block",
159
+ sourceType: "student",
160
+ reason: `Student PNP profile prohibits "${supportId}"`,
161
+ value: ctx.pnp.prohibitedSupports,
162
+ });
163
+ return;
164
+ }
165
+ const mandate = ctx.itemSettings?.requiredTools?.includes(supportId)
166
+ ? "item-requirement"
167
+ : ctx.districtPolicy?.requiredTools?.includes(supportId)
168
+ ? "district-requirement"
169
+ : undefined;
170
+ // 5. Test administration override `true`: grants the support for the
171
+ // session. A requirement naming it keeps its mandate.
172
+ if (override === true) {
173
+ if (mandate)
174
+ out.mandates.set(supportId, mandate);
175
+ out.perToolFlags.set(supportId, {
176
+ required: mandate !== undefined,
177
+ alwaysAvailable: true,
178
+ rule: "test-admin-override",
179
+ });
180
+ out.decisions.push({
181
+ precedence: 5,
182
+ rule: "test-admin-override",
183
+ featureId: supportId,
184
+ action: "enable",
185
+ sourceType: "assessment",
186
+ reason: `Test administrator enabled "${supportId}" for this session`,
187
+ value: ctx.testAdmin?.toolOverrides,
188
+ });
189
+ return;
190
+ }
191
+ // 6. Item requirement (forces enable)
192
+ if (mandate === "item-requirement") {
193
+ out.mandates.set(supportId, mandate);
194
+ out.perToolFlags.set(supportId, {
151
195
  required: true,
152
196
  alwaysAvailable: false,
153
- settings: this.resolveToolSettings(supportId, ctx),
154
197
  rule: "item-requirement",
155
- sourceType: "item",
156
198
  });
157
199
  out.decisions.push({
158
- precedence: 4,
200
+ precedence: 6,
159
201
  rule: "item-requirement",
160
- featureId: toolId,
202
+ featureId: supportId,
161
203
  action: "enable",
162
204
  sourceType: "item",
163
205
  reason: `Item requires "${supportId}" for this question`,
164
- value: ctx.itemSettings.requiredTools,
206
+ value: ctx.itemSettings?.requiredTools,
165
207
  });
166
208
  return;
167
209
  }
168
- // 5. District requirement
169
- if (ctx.districtPolicy?.requiredTools?.includes(supportId)) {
170
- const toolId = this.mapSupportToToolId(supportId, out);
171
- out.mandatedToolIds.add(toolId);
172
- out.perToolFlags.set(toolId, {
210
+ // 7. District requirement
211
+ if (mandate === "district-requirement") {
212
+ out.mandates.set(supportId, mandate);
213
+ out.perToolFlags.set(supportId, {
173
214
  required: true,
174
215
  alwaysAvailable: false,
175
- settings: this.resolveToolSettings(supportId, ctx),
176
216
  rule: "district-requirement",
177
- sourceType: "assessment",
178
217
  });
179
218
  out.decisions.push({
180
- precedence: 5,
219
+ precedence: 7,
181
220
  rule: "district-requirement",
182
- featureId: toolId,
221
+ featureId: supportId,
183
222
  action: "enable",
184
223
  sourceType: "assessment",
185
224
  reason: `District policy requires "${supportId}" for all assessments`,
186
- value: ctx.districtPolicy.requiredTools,
187
- });
188
- return;
189
- }
190
- // 6. PNP prohibitions and supports (student needs)
191
- if (ctx.pnp?.prohibitedSupports?.includes(supportId)) {
192
- const toolId = this.mapSupportToToolId(supportId, out);
193
- out.blockedToolIds.add(toolId);
194
- out.decisions.push({
195
- precedence: 6,
196
- rule: "pnp-prohibited",
197
- featureId: toolId,
198
- action: "block",
199
- sourceType: "student",
200
- reason: `Student PNP profile prohibits "${supportId}"`,
201
- value: ctx.pnp.prohibitedSupports,
225
+ value: ctx.districtPolicy?.requiredTools,
202
226
  });
203
227
  return;
204
228
  }
229
+ // 8. Profile supports (student needs)
205
230
  if (ctx.pnp?.supports?.includes(supportId)) {
206
- const toolId = this.mapSupportToToolId(supportId, out);
207
- out.perToolFlags.set(toolId, {
231
+ out.perToolFlags.set(supportId, {
208
232
  required: false,
209
233
  alwaysAvailable: true,
210
- settings: this.resolveToolSettings(supportId, ctx),
211
234
  rule: "pnp-support",
212
- sourceType: "student",
213
235
  });
214
236
  out.decisions.push({
215
- precedence: 6,
237
+ precedence: 8,
216
238
  rule: "pnp-support",
217
- featureId: toolId,
239
+ featureId: supportId,
218
240
  action: "enable",
219
241
  sourceType: "student",
220
242
  reason: `Student PNP profile requests "${supportId}"`,
@@ -222,16 +244,12 @@ export class PnpPolicySource {
222
244
  });
223
245
  return;
224
246
  }
225
- // 6 (skip): support-id mentioned somewhere but no rule fired.
226
- // Use the mapped tool id as `featureId` so the trail is keyed
227
- // the same way as every other branch — PR 5's debugger surfaces
228
- // index provenance trails by tool id and would otherwise show
229
- // orphaned support-id entries here (M8 PR 1 R3 S1).
230
- const toolId = this.mapSupportToToolId(supportId, out);
247
+ // 8 (skip): no rule fired. The trail is keyed by tool id, as in every
248
+ // other branch.
231
249
  out.decisions.push({
232
- precedence: 6,
250
+ precedence: 8,
233
251
  rule: "pnp-support",
234
- featureId: toolId,
252
+ featureId: supportId,
235
253
  action: "skip",
236
254
  sourceType: "system",
237
255
  reason: `Feature "${supportId}" not configured at any level`,
@@ -239,59 +257,42 @@ export class PnpPolicySource {
239
257
  });
240
258
  }
241
259
  /**
242
- * Map a QTI / PNP support id (e.g. `"calculator-basic"`) to the
243
- * registered tool id that owns it.
244
- *
245
- * Resolution rules:
246
- *
247
- * 1. **Multiple tools register the same support id:** the
248
- * *first-registered* tool wins. `ToolRegistry.getToolsByPNPSupport`
249
- * returns a `Set<string>` whose iteration order matches the
250
- * `register(...)` insertion order, so the result is
251
- * deterministic across runs but order-sensitive at registration
252
- * time. This is an unusual configuration — the typical case is
253
- * one tool per support id — but it is reachable when an
254
- * integrator overrides a default tool with a replacement that
255
- * claims the same `pnpSupportIds`. In that case the integrator
256
- * should `unregister(...)` the default before registering the
257
- * replacement.
258
- *
259
- * 2. **No tool registers the support id:** the support id is
260
- * returned verbatim as the `featureId` for provenance trails
261
- * and `ToolPolicyEntry.toolId`. This lets hosts use raw QTI
262
- * strings for tools the registry does not (yet) carry without
263
- * losing them in policy evaluation.
264
- *
265
- * `tests/policy/PnpPolicySource.test.ts` locks this mapping behavior.
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.
266
267
  */
267
- mapSupportToToolId(supportId, out) {
268
- const toolIds = this.toolRegistry.getToolsByPNPSupport(supportId);
269
- if (toolIds.size === 0) {
270
- // An empty registry means there is nothing to check the id against, not
271
- // that the id is wrong — reporting it there would fire on every support
272
- // id for a host that supplies no registry, which
273
- // `tool-config-validation` already warns about once.
274
- if (this.toolRegistry.getAllTools().length > 0) {
275
- out.unmappedSupportIds.add(supportId);
268
+ unknownSupportIds(assessment, items) {
269
+ const unmapped = new Map();
270
+ if (this.toolRegistry.getAllTools().length === 0)
271
+ return unmapped;
272
+ const pnp = assessment?.personalNeedsProfile;
273
+ const settings = assessment?.settings;
274
+ const named = [
275
+ ["district-block", settings?.districtPolicy?.blockedTools],
276
+ [
277
+ "test-admin-override",
278
+ Object.keys(settings?.testAdministration?.toolOverrides ?? {}),
279
+ ],
280
+ ["item-restriction", items.flatMap((item) => item.restrictedTools ?? [])],
281
+ ["pnp-prohibited", pnp?.prohibitedSupports],
282
+ ["item-requirement", items.flatMap((item) => item.requiredTools ?? [])],
283
+ ["district-requirement", settings?.districtPolicy?.requiredTools],
284
+ ["pnp-support", pnp?.supports],
285
+ ];
286
+ for (const [rule, ids] of named) {
287
+ for (const id of ids ?? []) {
288
+ if (this.toolRegistry.has(id))
289
+ continue;
290
+ const rules = unmapped.get(id) ?? [];
291
+ if (!rules.includes(rule))
292
+ rules.push(rule);
293
+ unmapped.set(id, rules);
276
294
  }
277
- return supportId;
278
295
  }
279
- return Array.from(toolIds)[0];
280
- }
281
- /**
282
- * Resolve tool-specific settings, item-level taking precedence over
283
- * assessment-level.
284
- *
285
- * Uses `??` (nullish-coalescing). Tool config values are typed `unknown` and may legitimately
286
- * be `false` / `0` / `""` (e.g. a tool whose canonical "off" config
287
- * is the literal `false`). `||` would treat those as missing and
288
- * silently fall through to the assessment-level config, masking the
289
- * authored item-level intent. `??` only falls through on
290
- * `null`/`undefined`, which matches the semantic of "value not
291
- * provided."
292
- */
293
- resolveToolSettings(supportId, ctx) {
294
- return (ctx.itemSettings?.toolParameters?.[supportId] ??
295
- ctx.toolConfigs?.[supportId]);
296
+ return unmapped;
296
297
  }
297
298
  }