@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
@@ -12,7 +12,8 @@
12
12
  * `tests/policy/compose-decision.test.ts` lock the orchestration in.
13
13
  */
14
14
  import { normalizeToolList, } from "../../services/tools-config-normalizer.js";
15
- import { ToolPolicyProvenanceBuilder } from "./provenance.js";
15
+ import { resolveToolParameters } from "./tool-parameters.js";
16
+ import { ToolPolicyProvenanceBuilder, } from "./provenance.js";
16
17
  export function composeDecision(inputs) {
17
18
  const { request, tools, pnpPolicy, customSources, contextId } = inputs;
18
19
  const builder = new ToolPolicyProvenanceBuilder(contextId);
@@ -66,7 +67,7 @@ export function composeDecision(inputs) {
66
67
  }
67
68
  return true;
68
69
  });
69
- // Step 3 — Host whitelist (only when non-empty).
70
+ // Step 3 — Host allowlist (only when non-empty).
70
71
  if (allowed.length > 0) {
71
72
  const allowedSet = new Set(allowed);
72
73
  candidates = candidates.filter((toolId) => {
@@ -103,23 +104,16 @@ export function composeDecision(inputs) {
103
104
  });
104
105
  // Step 5 — PNP/profile policy gates.
105
106
  const diagnostics = [];
106
- const sourcesByTool = new Map();
107
- for (const toolId of candidates) {
108
- sourcesByTool.set(toolId, ["placement"]);
109
- }
110
- // Snapshot the post-host candidate set so step 5b can distinguish
111
- // "host removed a required tool" (requiredToolBlocked fires)
112
- // from "profile policy's own precedence removed a required tool" (fix
113
- // per M8 PR 1 R1 S1 — multi-rule conflict on the same tool must
114
- // not blame the host).
107
+ // Snapshot the post-host candidate set so step 5b reports
108
+ // requiredToolBlocked only for a mandated tool the host removed.
115
109
  const postHostCandidates = new Set(candidates);
116
110
  let pnpPolicyResult = null;
117
111
  if (pnpPolicy.enforcement === "on" &&
118
112
  pnpPolicy.source &&
119
- (pnpPolicy.assessment || pnpPolicy.currentItemRef)) {
113
+ (pnpPolicy.assessment || pnpPolicy.item)) {
120
114
  pnpPolicyResult = pnpPolicy.source.apply({
121
115
  assessment: pnpPolicy.assessment,
122
- currentItemRef: pnpPolicy.currentItemRef,
116
+ item: pnpPolicy.item,
123
117
  });
124
118
  if (pnpPolicyResult.sources.assessment) {
125
119
  builder.addSource("assessment", pnpPolicyResult.sources.assessment);
@@ -141,29 +135,12 @@ export function composeDecision(inputs) {
141
135
  value: decision.value,
142
136
  });
143
137
  }
144
- // A support id no registration claims resolves to itself and then matches
145
- // nothing placed, so the capability is absent with no trace of why — the
146
- // failure a host sending its own vocabulary instead of an AfA/QTI feature
147
- // id actually hits. Reported per decision rather than at config time
148
- // because the ids arrive with the profile, not with the tools config.
149
- for (const supportId of pnpPolicyResult.unmappedSupportIds) {
150
- diagnostics.push({
151
- code: "tool-policy.unknownSupportId",
152
- level: request.level,
153
- toolId: supportId,
154
- message: `No registered tool claims PNP support id "${supportId}"; it was carried through as a feature id and matched nothing. Expected an AfA 3.0 / QTI 3.0 access feature id.`,
155
- source: "pnp.pnp-support",
156
- });
138
+ for (const [toolId, rule] of pnpPolicyResult.blockedOverrides) {
139
+ diagnostics.push(overrideBlockedDiagnostic(toolId, rule, request.level));
157
140
  }
158
141
  // 5a — remove PNP/profile-blocked tools from the candidate set.
159
- candidates = candidates.filter((toolId) => {
160
- return !pnpPolicyResult.blockedToolIds.has(toolId);
161
- });
162
- for (const toolId of Array.from(sourcesByTool.keys())) {
163
- if (pnpPolicyResult.blockedToolIds.has(toolId)) {
164
- sourcesByTool.delete(toolId);
165
- }
166
- }
142
+ const { blockedToolIds } = pnpPolicyResult;
143
+ candidates = candidates.filter((toolId) => !blockedToolIds.has(toolId));
167
144
  // 5b — surface requiredToolBlocked diagnostics ONLY for
168
145
  // PNP/profile-mandated tools that the host removed (i.e., the tool was
169
146
  // NOT in `postHostCandidates`). Mandated tools removed by
@@ -171,20 +148,24 @@ export function composeDecision(inputs) {
171
148
  // blocking a `requiredTools` mandate) are an internal policy
172
149
  // resolution and must not be reported as a host conflict — the
173
150
  // loser's `block` decision is already in the trail with full
174
- // provenance.
151
+ // provenance. A mandate names no level, so a tool placed at another
152
+ // level is served there: only placement at no level is a removal.
175
153
  //
176
154
  // `details` carries `{ rule, hostRule, hostValue }` so consumers
177
155
  // can render the conflict as a single sentence without
178
156
  // re-deriving the host gate from `provenance.features`. See
179
157
  // `RequiredToolBlockedDetails` for the typed shape.
180
- for (const mandatedToolId of pnpPolicyResult.mandatedToolIds) {
181
- const removedByHost = !postHostCandidates.has(mandatedToolId);
182
- if (!removedByHost)
158
+ const placedAtAnyLevel = new Set(Object.values(tools.placement).flatMap(normalizeToolList));
159
+ for (const [mandatedToolId, ruleName] of pnpPolicyResult.mandates) {
160
+ if (postHostCandidates.has(mandatedToolId))
183
161
  continue;
184
- const flag = pnpPolicyResult.perToolFlags.get(mandatedToolId);
185
- const ruleName = flag?.rule ?? "required-tool";
162
+ if (!placement.includes(mandatedToolId) &&
163
+ placedAtAnyLevel.has(mandatedToolId)) {
164
+ continue;
165
+ }
186
166
  const hostGate = detectHostRemovalGate(mandatedToolId, {
187
- placement,
167
+ placedAtAnyLevel,
168
+ allTools: tools.placement,
188
169
  allowed,
189
170
  blocked,
190
171
  providers: tools.providers,
@@ -198,7 +179,9 @@ export function composeDecision(inputs) {
198
179
  code: "tool-policy.requiredToolBlocked",
199
180
  level: request.level,
200
181
  toolId: mandatedToolId,
201
- message: `Profile policy mandates "${mandatedToolId}" (${ruleName}) but host policy removed it from level "${request.level}" via ${hostGate.hostRule}.`,
182
+ message: hostGate.hostRule === "placement-missing"
183
+ ? `Profile policy mandates "${mandatedToolId}" (${ruleName}), but no level of tools.placement lists it.`
184
+ : `Profile policy mandates "${mandatedToolId}" (${ruleName}), but host policy removes it via ${hostGate.hostRule}.`,
202
185
  details,
203
186
  });
204
187
  builder.addDecision({
@@ -211,15 +194,6 @@ export function composeDecision(inputs) {
211
194
  value: details,
212
195
  });
213
196
  }
214
- // 5c — attach PNP policy source tags to surviving entries.
215
- for (const toolId of candidates) {
216
- const flag = pnpPolicyResult.perToolFlags.get(toolId);
217
- if (flag) {
218
- const tags = sourcesByTool.get(toolId) ?? [];
219
- tags.push(`pnp.${flag.rule}`);
220
- sourcesByTool.set(toolId, tags);
221
- }
222
- }
223
197
  }
224
198
  // Step 6 — Custom PolicySources.
225
199
  for (let i = 0; i < customSources.length; i++) {
@@ -238,7 +212,7 @@ export function composeDecision(inputs) {
238
212
  level: request.level,
239
213
  toolId,
240
214
  message: `Custom source "${source.id}" tried to add tool "${toolId}" that was not in candidates; ignored.`,
241
- source: `custom.${source.id}`,
215
+ details: { customSourceId: source.id },
242
216
  });
243
217
  refinedSet.delete(toolId);
244
218
  }
@@ -250,7 +224,7 @@ export function composeDecision(inputs) {
250
224
  // this auto-log, the provenance trail for a silently-removed
251
225
  // tool would show only the prior `enable` decisions and the
252
226
  // final-state reconciliation would have no evidence to mark it
253
- // `blocked` (M8 PR 1 R2 S1 / M1 case B).
227
+ // `blocked`.
254
228
  const explicitBlocks = new Set();
255
229
  if (result.decisions) {
256
230
  for (const event of result.decisions) {
@@ -284,18 +258,27 @@ export function composeDecision(inputs) {
284
258
  value: { customSourceId: source.id },
285
259
  });
286
260
  }
287
- // Annotate surviving entries with `custom.<id>` so hosts can see
288
- // which custom source kept them in.
289
- for (const toolId of candidates) {
290
- const tags = sourcesByTool.get(toolId) ?? ["placement"];
291
- tags.push(`custom.${source.id}`);
292
- sourcesByTool.set(toolId, tags);
293
- }
294
- // And drop attribution for IDs the source removed.
295
- for (const toolId of before) {
296
- if (!refinedSet.has(toolId)) {
297
- sourcesByTool.delete(toolId);
298
- }
261
+ }
262
+ // Step 7 — item settings a shared toolbar does not apply. Placement is the
263
+ // host's section-wide choice, so the tool stays; the diagnostic says where to
264
+ // place it for the item setting to hold.
265
+ for (const toolId of candidates) {
266
+ for (const item of inputs.unappliedItems ?? []) {
267
+ const settings = ["restrictedTools", "requiredTools"].filter((key) => item.settings[key]?.includes(toolId) === true);
268
+ if (settings.length === 0)
269
+ continue;
270
+ const details = {
271
+ itemId: item.id,
272
+ settings,
273
+ toolbarLevel: request.scope.level,
274
+ };
275
+ diagnostics.push({
276
+ code: "tool-policy.itemSettingNotApplied",
277
+ level: request.level,
278
+ toolId,
279
+ message: `Item "${item.id}" names "${toolId}" in ${settings.join(" and ")}, but "${toolId}" is on the ${request.scope.level}-level toolbar, which item settings do not reach. Place "${toolId}" at item level to enforce the setting per item.`,
280
+ details,
281
+ });
299
282
  }
300
283
  }
301
284
  const survivingIds = new Set(candidates);
@@ -305,8 +288,7 @@ export function composeDecision(inputs) {
305
288
  toolId,
306
289
  required: flag?.required ?? false,
307
290
  alwaysAvailable: flag?.alwaysAvailable ?? false,
308
- settings: flag?.settings,
309
- sources: sourcesByTool.get(toolId) ?? ["placement"],
291
+ parameters: resolveToolParameters(toolId, pnpPolicy.assessment, pnpPolicy.item),
310
292
  };
311
293
  });
312
294
  builder.reconcileFinalStates(survivingIds);
@@ -323,10 +305,11 @@ export function composeDecision(inputs) {
323
305
  * (blocklist) last — so the FIRST gate that would have matched wins,
324
306
  * which is the gate the pipeline actually invoked. Subsequent gates
325
307
  * never get to evaluate a tool that an earlier step already removed.
308
+ * Step 5b passes only a tool this level places or no level places.
326
309
  */
327
310
  function detectHostRemovalGate(toolId, args) {
328
- if (!args.placement.includes(toolId)) {
329
- return { hostRule: "placement-missing", hostValue: args.placement };
311
+ if (!args.placedAtAnyLevel.has(toolId)) {
312
+ return { hostRule: "placement-missing", hostValue: args.allTools };
330
313
  }
331
314
  if (args.providers?.[toolId]?.enabled === false) {
332
315
  return { hostRule: "provider-disabled", hostValue: false };
@@ -340,9 +323,47 @@ function detectHostRemovalGate(toolId, args) {
340
323
  hostValue: Array.from(args.blocked),
341
324
  };
342
325
  }
343
- // Defensive: `removedByHost` is true at the call site (the tool is
344
- // in `mandatedToolIds` but not in `postHostCandidates`), so one of
345
- // the four gates above MUST have fired. If none did, the host
346
- // pipeline has a bug; surface a best-effort rather than throwing.
347
- return { hostRule: "placement-missing", hostValue: args.placement };
326
+ // Defensive: at the call site the tool is in `mandates` but not in
327
+ // `postHostCandidates`, so one of the four gates above MUST have
328
+ // fired. If none did, the host pipeline has a bug; surface a
329
+ // best-effort rather than throwing.
330
+ return { hostRule: "placement-missing", hostValue: args.allTools };
331
+ }
332
+ /** Where each PNP/profile rule's ids are authored. */
333
+ const PNP_RULE_FIELDS = {
334
+ "district-block": "settings.districtPolicy.blockedTools",
335
+ "test-admin-override": "settings.testAdministration.toolOverrides",
336
+ "item-restriction": "item settings.restrictedTools",
337
+ "pnp-prohibited": "personalNeedsProfile.prohibitedSupports",
338
+ "item-requirement": "item settings.requiredTools",
339
+ "district-requirement": "settings.districtPolicy.requiredTools",
340
+ "pnp-support": "personalNeedsProfile.supports",
341
+ };
342
+ /**
343
+ * A `tool-policy.overrideBlocked` diagnostic for a tool a `true` test
344
+ * administration override grants and `rule` withdraws. `level` is absent on a
345
+ * feature decision, which has no toolbar level.
346
+ */
347
+ export function overrideBlockedDiagnostic(toolId, rule, level) {
348
+ return {
349
+ code: "tool-policy.overrideBlocked",
350
+ ...(level ? { level } : {}),
351
+ toolId,
352
+ message: `settings.testAdministration.toolOverrides grants "${toolId}", but ${PNP_RULE_FIELDS[rule]} withdraws it and outranks a granting override.`,
353
+ details: { rule },
354
+ };
355
+ }
356
+ /**
357
+ * A `tool-policy.unknownSupportId` diagnostic for an id the registry lacks,
358
+ * naming every list that names it. It describes the engine's inputs rather
359
+ * than a decision, so it carries no level.
360
+ */
361
+ export function unknownSupportIdDiagnostic(supportId, origins) {
362
+ const fields = origins.map((rule) => PNP_RULE_FIELDS[rule]).join(", ");
363
+ return {
364
+ code: "tool-policy.unknownSupportId",
365
+ toolId: supportId,
366
+ message: `No tool is registered under "${supportId}", named in ${fields}, so it matches nothing. These lists name tools by tool id.`,
367
+ details: { origins: [...origins] },
368
+ };
348
369
  }
@@ -11,16 +11,15 @@
11
11
  */
12
12
  import type { ToolPlacementLevel } from "../../services/tools-config-normalizer.js";
13
13
  import type { ToolContext, ToolLevel } from "../../services/tool-context.js";
14
- import type { PolicySourceTag } from "./policy-source-tag.js";
15
- import type { ToolPolicyProvenance } from "./provenance.js";
14
+ import type { PnpPolicySourceRule, ToolPolicyProvenance } from "./provenance.js";
16
15
  /**
17
16
  * Scope identifier for a `decide(...)` request. The engine uses
18
17
  * `level` to pick which placement bucket to read; `scopeId` is opaque
19
18
  * to the engine (it is round-tripped into provenance for human-readable
20
19
  * trails) and is typically the section-id, item-id, or passage-id of
21
20
  * the surface asking the question. `contentKind` mirrors the
22
- * `ToolbarContext.scope.contentKind` field already in use by
23
- * `<pie-item-toolbar>` Pass-2 visibility.
21
+ * `ToolbarContext.scope.contentKind` field `<pie-item-toolbar>` reads for
22
+ * content relevance.
24
23
  */
25
24
  export interface ToolScope {
26
25
  level: ToolLevel;
@@ -41,29 +40,49 @@ export interface ToolPolicyDecisionRequest {
41
40
  level: ToolPlacementLevel;
42
41
  scope: ToolScope;
43
42
  /**
44
- * Optional Pass-2 context. The engine itself does NOT call
45
- * `isVisibleInContext(...)`; that gate is intentionally applied
46
- * at the toolbar boundary (Pass-2 = relevance, Pass-1 = the
47
- * engine's job). The context is forwarded to custom
48
- * `PolicySource` instances so they can refine the candidate set
49
- * if they need to.
43
+ * Optional tool context. The engine does not call
44
+ * `isVisibleInContext(...)`: content relevance is applied at the toolbar
45
+ * boundary. The context is forwarded to custom `PolicySource` instances so
46
+ * they can refine the candidate set.
50
47
  */
51
48
  context?: ToolContext;
52
49
  }
53
50
  /**
54
- * Diagnostic codes the engine may emit alongside a decision. The
55
- * canonical case in M8 is `tool-policy.requiredToolBlocked` — fired
56
- * when a host policy gate (`policy.blocked`, missing-from-`placement`,
57
- * provider veto) removed a tool that profile policy `requiredTools` /
58
- * `district.requiredTools` mandates.
51
+ * Diagnostic codes the engine may emit alongside a decision.
52
+ * `tool-policy.requiredToolBlocked` fires when a host policy gate
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}.
59
57
  *
60
58
  * `tool-policy.placementMissing` fires when a custom `PolicySource`
61
59
  * references a tool ID that is not present in `tools.placement[level]`
62
- * for the resolved level. The host-side `ToolConfigDiagnostic` channel
60
+ * for the resolved level. `details` is {@link PlacementMissingDetails}.
61
+ * The host-side `ToolConfigDiagnostic` channel
63
62
  * (already used by `tool-config-validation.ts`) covers config-time
64
63
  * misconfiguration; this channel covers per-decision conflicts.
64
+ *
65
+ * `tool-policy.unknownSupportId` fires for each id a profile, district
66
+ * policy, test administration or item names that no tool is registered under.
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}.
70
+ *
71
+ * `tool-policy.itemSettingNotApplied` fires on a section-, assessment- or
72
+ * passage-level decision for each tool on that toolbar a mounted item's
73
+ * `restrictedTools` or `requiredTools` names, once per tool and item: item
74
+ * settings govern only the item's own toolbar. `details` is
75
+ * {@link ItemSettingNotAppliedDetails}.
76
+ *
77
+ * `tool-policy.overrideBlocked` fires for each tool a `true`
78
+ * `settings.testAdministration.toolOverrides` entry grants that an item's
79
+ * `restrictedTools` or the profile's `prohibitedSupports` withdraws instead.
80
+ * `details` is {@link OverrideBlockedDetails}.
81
+ *
82
+ * The toolkit coordinator logs each diagnostic once per code, tool and item,
83
+ * and hands the same ones to its `onPolicyDiagnostic` listeners.
65
84
  */
66
- export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool-policy.placementMissing" | "tool-policy.unknownSupportId";
85
+ export type ToolPolicyDiagnosticCode = keyof ToolPolicyDiagnosticDetails;
67
86
  /**
68
87
  * Which host gate removed a profile-mandated tool. Surfaced inside
69
88
  * `ToolPolicyDiagnostic.details` for `tool-policy.requiredToolBlocked`
@@ -73,66 +92,90 @@ export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool
73
92
  * `provenance.features[toolId].allDecisions`.
74
93
  *
75
94
  * The four values mirror steps 1–4 of `composeDecision`:
76
- * - `placement-missing` — tool is not in `tools.placement[level]`
95
+ * - `placement-missing` — no level of `tools.placement` lists the tool
77
96
  * - `provider-disabled` — `tools.providers[id].enabled === false`
78
97
  * - `host-allowlist` — non-empty `tools.policy.allowed` excludes the id
79
98
  * - `host-blocked` — `tools.policy.blocked` lists the id
80
99
  */
81
100
  export type ToolPolicyHostGate = "placement-missing" | "provider-disabled" | "host-allowlist" | "host-blocked";
82
- /**
83
- * Strongly-typed payload for `tool-policy.requiredToolBlocked`
84
- * diagnostics. The diagnostic's `details` field still types as
85
- * `Record<string, unknown>` for forward compatibility, but engines
86
- * always populate this exact shape today and consumers can safely
87
- * cast.
88
- */
89
- export interface RequiredToolBlockedDetails extends Record<string, unknown> {
90
- /** The profile policy rule that mandated the tool (e.g. `district-requirement`). */
91
- 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";
92
105
  /** Which host gate removed the tool. */
93
106
  hostRule: ToolPolicyHostGate;
94
107
  /** The host configuration value that triggered the gate (best-effort, may be omitted). */
95
108
  hostValue?: unknown;
96
109
  }
97
- export interface ToolPolicyDiagnostic {
98
- code: ToolPolicyDiagnosticCode;
99
- level: ToolPlacementLevel;
100
- toolId: string;
101
- message: string;
102
- source?: PolicySourceTag;
103
- details?: Record<string, unknown>;
110
+ /** Payload of a `tool-policy.unknownSupportId` diagnostic. */
111
+ export interface UnknownSupportIdDetails {
112
+ /** The rules whose lists name the id, in precedence order. */
113
+ origins: PnpPolicySourceRule[];
114
+ }
115
+ /** Payload of a `tool-policy.placementMissing` diagnostic. */
116
+ export interface PlacementMissingDetails {
117
+ /** The custom `PolicySource` that named the tool. */
118
+ customSourceId: string;
119
+ }
120
+ /** Payload of a `tool-policy.overrideBlocked` diagnostic. */
121
+ export interface OverrideBlockedDetails {
122
+ /** The rule that withdrew the granted tool. */
123
+ rule: "item-restriction" | "pnp-prohibited";
124
+ }
125
+ /** Payload of a `tool-policy.itemSettingNotApplied` diagnostic. */
126
+ export interface ItemSettingNotAppliedDetails {
127
+ /** Canonical id of the item whose setting names the tool. */
128
+ itemId: string;
129
+ /** The item settings that name it. */
130
+ settings: Array<"restrictedTools" | "requiredTools">;
131
+ /** The level of the toolbar the tool is on. */
132
+ toolbarLevel: ToolLevel;
104
133
  }
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;
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];
105
156
  export interface ToolPolicyEntry {
106
157
  toolId: string;
107
158
  /**
108
159
  * `true` when PNP/profile policy mandates this tool (item or district
109
- * `requiredTools`, or PNP support without prohibition). Advisory
110
- * mandates that the host blocked do *not* surface here — they
111
- * appear in `diagnostics` instead. Hosts that need to know about
112
- * those should listen on the diagnostic channel.
160
+ * `requiredTools`). Advisory mandates that the host blocked do *not*
161
+ * surface here — they appear in `diagnostics` instead. Hosts that need
162
+ * to know about those should listen on the diagnostic channel.
113
163
  */
114
164
  required: boolean;
115
165
  /**
116
- * `true` for PNP `supports` — UI-level signal that the host
117
- * cannot toggle this tool off in user preferences. Does NOT
118
- * override host blocks (they would have removed the entry
119
- * before this flag is read).
166
+ * `true` for a tool the student's PNP `supports` or a
167
+ * `testAdministration.toolOverrides` entry grants — UI-level signal that
168
+ * the host cannot toggle this tool off in user preferences. Does NOT
169
+ * override host blocks (they would have removed the entry before this
170
+ * flag is read).
120
171
  */
121
172
  alwaysAvailable: boolean;
122
173
  /**
123
- * Tool-specific settings resolved from the assessment / item
124
- * settings via the PNP policy source. Hosts that want to read these
125
- * directly should use `entry.settings` rather than peeking into
126
- * the assessment entity from above the engine.
127
- */
128
- settings?: unknown;
129
- /**
130
- * Every contributor that helped this entry survive the
131
- * composition pipeline. Always non-empty — at minimum
132
- * `["placement"]` for any entry that came from
133
- * `tools.placement[level]`.
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.
134
177
  */
135
- sources: PolicySourceTag[];
178
+ parameters?: Record<string, unknown>;
136
179
  }
137
180
  export interface ToolPolicyDecision {
138
181
  visibleTools: ToolPolicyEntry[];
@@ -21,20 +21,20 @@
21
21
  * reasons, and only the second is a defect.
22
22
  */
23
23
  import type { PnpPolicyResult } from "../sources/PnpPolicySource.js";
24
- import type { PnpPolicySourceRule } from "./policy-source-tag.js";
25
- import type { ToolPolicyResolutionDecision, ToolPolicySourceType } from "./provenance.js";
24
+ import type { ToolPolicyDiagnostic } from "./decision-types.js";
25
+ import type { PnpPolicySourceRule, ToolPolicyResolutionDecision, ToolPolicySourceType } from "./provenance.js";
26
26
  /**
27
- * A feature verdict comes from one of the six PNP precedence levels, or from a
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
  /**
37
- * `true` only when policy explicitly granted the feature at one of the six
37
+ * `true` only when policy explicitly granted the feature at one of the eight
38
38
  * precedence levels. A feature nobody configured is **not** available —
39
39
  * accommodations require a documented need, so silence means no.
40
40
  */
@@ -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;
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;
@@ -51,9 +52,10 @@ export interface FeaturePolicyDecision {
51
52
  *
52
53
  * `false` makes a *denial* a wiring gap rather than a verdict: no profile,
53
54
  * district policy or test administration could be consulted, because there was
54
- * no assessment to read them from. It is not itself a denial — an item ref
55
- * carrying `requiredTools` mandates a feature at precedence 4 with no
56
- * assessment bound — so read it alongside `granted` rather than instead of it.
55
+ * no assessment to read them from. It is not itself a denial — an item's
56
+ * registered settings carrying `requiredTools` mandate a feature at precedence
57
+ * 6 with no assessment bound — so read it alongside `granted` rather than
58
+ * instead of it.
57
59
  *
58
60
  * Granting is unaffected either way: an unbound host with no item mandate still
59
61
  * gets `granted: false`, since an accommodation requires a documented need and
@@ -65,16 +67,23 @@ export interface FeaturePolicyDecision {
65
67
  */
66
68
  assessmentBound: boolean;
67
69
  /**
68
- * `true` when the grant is a mandate (item or district `requiredTools`)
69
- * rather than a student-profile support.
70
+ * `true` when item or district `requiredTools` mandates the feature, including
71
+ * under a `true` test-administration override.
70
72
  */
71
73
  required: boolean;
72
74
  /**
73
- * Feature parameters resolved from item `toolParameters` then assessment
74
- * `toolConfigs`, keyed by the feature id. The seam a later configurable
75
- * 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.
76
78
  */
77
- parameters?: unknown;
79
+ parameters?: P;
80
+ /**
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`.
85
+ */
86
+ diagnostics: ToolPolicyDiagnostic[];
78
87
  }
79
88
  /** What the engine knows that a single support-id resolution does not. */
80
89
  export interface FeatureDecisionContext {
@@ -90,7 +99,7 @@ export interface FeatureDecisionContext {
90
99
  * blocklist is a verdict whether or not an assessment was bound, and the flag
91
100
  * only ever qualifies a *policy* denial.
92
101
  */
93
- 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>;
94
103
  /**
95
104
  * Whether a host gate produced the verdict, rather than a policy source.
96
105
  *
@@ -107,4 +116,4 @@ export declare function isHostDeniedFeature(decision: Pick<FeaturePolicyDecision
107
116
  * exactly one decision and at most one flags entry — no mapped-tool-id
108
117
  * bookkeeping is needed to read it back out.
109
118
  */
110
- 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,13 +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 } from "./compose-decision.js";
23
24
  /**
24
25
  * Reason text for a denial that had no assessment to decide against.
25
26
  *
26
- * Replaces the `pnp-support` skip's "not configured at any level", which is
27
- * true but reads as a completed evaluation. `rule` and `precedence` stay as the
28
- * source reported them: nothing fired, so naming a seventh rule would describe
29
- * a precedence level that does not exist.
27
+ * Used in place of "not configured", which is true but reads as a completed
28
+ * evaluation.
30
29
  */
31
30
  const unboundAssessmentReason = (featureId) => `No assessment is bound, so no policy source could grant "${featureId}"`;
32
31
  /**
@@ -51,6 +50,7 @@ export function hostFeatureDenial(featureId, rule, hostValue, context) {
51
50
  : `Not listed in tools.policy.allowed (${hostValue.join(", ")})`,
52
51
  required: false,
53
52
  assessmentBound: context.assessmentBound,
53
+ diagnostics: [],
54
54
  };
55
55
  }
56
56
  /**
@@ -71,27 +71,31 @@ export function isHostDeniedFeature(decision) {
71
71
  * exactly one decision and at most one flags entry — no mapped-tool-id
72
72
  * bookkeeping is needed to read it back out.
73
73
  */
74
- export function interpretFeatureResult(featureId, result, context) {
74
+ export function interpretFeatureResult(featureId, result, context, parameters) {
75
75
  const decision = result.decisions[0];
76
76
  const flags = Array.from(result.perToolFlags.values())[0];
77
77
  const granted = decision?.action === "enable";
78
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";
79
82
  return {
80
83
  featureId,
81
84
  granted,
82
85
  action: decision?.action ?? "skip",
83
- rule: decision?.rule ?? "pnp-support",
84
- precedence: decision?.precedence ?? 6,
86
+ rule: fired ? decision.rule : "none",
87
+ precedence: fired ? decision.precedence : null,
85
88
  sourceType: decision?.sourceType ?? "system",
86
89
  // Only a denial is re-worded. An unbound host can still be granted the
87
- // feature — an item ref carrying `requiredTools` mandates it at precedence 4
88
- // with no assessment in sight — and there the source's reason is the true
89
- // one.
90
+ // feature — an item's registered settings carrying `requiredTools` mandate
91
+ // it at precedence 6 with no assessment in sight — and there the source's
92
+ // reason is the true one.
90
93
  reason: context.assessmentBound || granted
91
94
  ? reason
92
95
  : unboundAssessmentReason(featureId),
93
96
  required: Boolean(flags?.required),
94
- parameters: flags?.settings,
97
+ parameters,
95
98
  assessmentBound: context.assessmentBound,
99
+ diagnostics: Array.from(result.blockedOverrides, ([toolId, rule]) => overrideBlockedDiagnostic(toolId, rule)),
96
100
  };
97
101
  }