@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
@@ -3,24 +3,24 @@
3
3
  * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Wraps `composeDecision(...)` with engine-instance state:
6
- * - bound inputs (`tools`, `assessment`, `currentItemRef`, ...)
6
+ * - bound inputs (`tools`, `assessment`, `pnpEnforcement`) and the
7
+ * settings of the mounted items
7
8
  * - registered custom `PolicySource`s
8
9
  * - `onPolicyChange` subscriber bus
9
10
  * - `dispose()` clean-up
10
11
  *
11
12
  * The class has no DOM dependency and no Svelte runes. It is safe to
12
13
  * instantiate inside a worker, in a Node test, or inside a CE
13
- * connectedCallback. The Svelte context glue (`TOOL_POLICY_ENGINE_KEY`)
14
- * lives in `engine-context.ts`.
15
- *
16
- * PR 1 ships the engine *without callers*. PR 2 wires it into
17
- * `ToolkitCoordinator`. PR 3 switches `<pie-item-toolbar>` over.
14
+ * connectedCallback. `ToolkitCoordinator` owns the instance, and
15
+ * `<pie-item-toolbar>` reads its decisions through the coordinator.
18
16
  */
19
17
  import { normalizeToolList, normalizeToolsConfig, } from "../../services/tools-config-normalizer.js";
20
18
  import { hostFeatureDenial, interpretFeatureResult, } from "./feature-decision.js";
21
- import { composeDecision } from "./compose-decision.js";
19
+ import { composeDecision, unknownSupportIdDiagnostic, } from "./compose-decision.js";
22
20
  import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
23
- import { PnpPolicySource } from "../sources/PnpPolicySource.js";
21
+ import { resolveToolParameters } from "./tool-parameters.js";
22
+ import { structurallyEqual } from "../../utils/structural-equality.js";
23
+ import { PnpPolicySource, } from "../sources/PnpPolicySource.js";
24
24
  const DEFAULT_TOOLS = normalizeToolsConfig({
25
25
  policy: { allowed: [], blocked: [] },
26
26
  placement: { section: [], item: [], passage: [] },
@@ -28,30 +28,34 @@ const DEFAULT_TOOLS = normalizeToolsConfig({
28
28
  });
29
29
  export class ToolPolicyEngine {
30
30
  toolRegistry;
31
+ unsubscribeRegistry;
31
32
  contextId;
33
+ assessmentOptional;
32
34
  pnpPolicySource;
33
35
  customSources;
34
36
  listeners = new Set();
35
37
  tools;
36
38
  assessment;
37
- currentItemRef;
38
- pnpEnforcement;
39
+ pnpEnforcementOverride;
40
+ /**
41
+ * Mounted items' settings by canonical item id, newest registration last;
42
+ * see {@link registerItemSettings}.
43
+ */
44
+ itemSettings = new Map();
45
+ inputDiagnostics = Object.freeze([]);
39
46
  disposed = false;
40
47
  constructor(args) {
41
48
  this.toolRegistry = args.toolRegistry;
42
49
  this.contextId = args.contextId ?? "tool-policy";
50
+ this.assessmentOptional = args.assessmentOptional === true;
43
51
  this.pnpPolicySource = new PnpPolicySource(this.toolRegistry);
44
52
  this.customSources = args.customSources ? [...args.customSources] : [];
45
53
  const inputs = args.inputs ?? {};
46
54
  this.tools = inputs.tools ?? DEFAULT_TOOLS;
47
55
  this.assessment = inputs.assessment ?? null;
48
- this.currentItemRef = inputs.currentItemRef ?? null;
49
- this.pnpEnforcement =
50
- inputs.pnpEnforcement ??
51
- resolveDefaultPnpEnforcement({
52
- assessment: this.assessment,
53
- currentItemRef: this.currentItemRef,
54
- });
56
+ this.pnpEnforcementOverride = inputs.pnpEnforcement ?? null;
57
+ this.refreshInputDiagnostics();
58
+ this.unsubscribeRegistry = this.watchRegistry();
55
59
  }
56
60
  /**
57
61
  * Resolve the visible tool set for a given placement level and
@@ -67,27 +71,92 @@ export class ToolPolicyEngine {
67
71
  * values even though the policy decision itself is identical.
68
72
  * Hosts that want timestamp-stable provenance should reuse a
69
73
  * cached `ToolPolicyDecision` rather than re-call `decide(...)`.
74
+ *
75
+ * An item's registered settings apply to the item's own toolbar: a request
76
+ * at item level whose scope is the item. Every other request — a section,
77
+ * assessment or passage toolbar — is shared by the items it sits beside, so
78
+ * it leaves every item's settings out and reports each tool on its toolbar
79
+ * that one restricts or requires as `tool-policy.itemSettingNotApplied`.
70
80
  */
71
81
  decide(request) {
72
82
  this.assertNotDisposed();
73
83
  const requestContextId = request.scope.scopeId
74
84
  ? `${this.contextId}:${request.level}:${request.scope.scopeId}`
75
85
  : `${this.contextId}:${request.level}`;
86
+ const item = request.level === "item" ? this.itemForScope(request.scope) : undefined;
87
+ const shared = request.level !== "item" ||
88
+ request.scope.level === "section" ||
89
+ request.scope.level === "assessment";
76
90
  return composeDecision({
77
91
  request,
78
92
  tools: this.tools,
79
93
  pnpPolicy: {
80
94
  source: this.pnpPolicySource,
81
95
  assessment: this.assessment ?? undefined,
82
- currentItemRef: this.currentItemRef ?? undefined,
83
- enforcement: this.pnpEnforcement,
96
+ item,
97
+ enforcement: this.enforcementFor(item),
84
98
  },
99
+ unappliedItems: shared ? this.enforcedItems() : undefined,
85
100
  customSources: this.customSources,
86
101
  contextId: requestContextId,
87
102
  });
88
103
  }
89
104
  /**
90
- * Resolve eligibility for one PNP/AfA feature id through the six-level
105
+ * Whether some surface is granted `featureId`: a decision with no item scope,
106
+ * or one scoped to a registered item. A provider serves every surface, so its
107
+ * failure denies the accommodation wherever one surface is granted it.
108
+ *
109
+ * `enforced` counts only scopes whose `pnpEnforcement` is on, as a toolbar
110
+ * tool's decisions do; a feature with no placement leaves it unset.
111
+ */
112
+ grantsFeatureAnywhere(featureId, options = {}) {
113
+ const counts = (item) => !options.enforced || this.enforcementFor(item) === "on";
114
+ if (counts() && this.decideFeature(featureId).granted)
115
+ return true;
116
+ for (const id of this.itemSettings.keys()) {
117
+ const settings = this.itemSettingsFor(id);
118
+ if (!counts(settings ? { id, settings } : undefined))
119
+ continue;
120
+ if (this.decideFeature(featureId, { level: "item", scopeId: id }).granted) {
121
+ return true;
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+ /**
127
+ * File a mounted item's settings under its canonical id. They govern the
128
+ * decisions scoped to that item: its own toolbar, and a feature its content
129
+ * asks about with the item's scope. See {@link decide}.
130
+ *
131
+ * The same item mounted twice registers twice; the newest registration
132
+ * applies, and its cleanup hands the item back to the one before. Emits an
133
+ * `"item-settings"` change whenever the settings that apply to the item
134
+ * change.
135
+ */
136
+ registerItemSettings(itemId, settings) {
137
+ this.assertNotDisposed();
138
+ const entry = { settings };
139
+ const before = this.itemSettingsFor(itemId);
140
+ const entries = this.itemSettings.get(itemId) ?? [];
141
+ entries.push(entry);
142
+ this.itemSettings.set(itemId, entries);
143
+ this.emitIfItemSettingsChanged(itemId, before);
144
+ return () => {
145
+ if (this.disposed)
146
+ return;
147
+ const current = this.itemSettings.get(itemId);
148
+ const index = current?.indexOf(entry) ?? -1;
149
+ if (!current || index < 0)
150
+ return;
151
+ const previous = this.itemSettingsFor(itemId);
152
+ current.splice(index, 1);
153
+ if (current.length === 0)
154
+ this.itemSettings.delete(itemId);
155
+ this.emitIfItemSettingsChanged(itemId, previous);
156
+ };
157
+ }
158
+ /**
159
+ * Resolve eligibility for one PNP/AfA feature id through the eight-level
91
160
  * precedence, independent of toolbar placement.
92
161
  *
93
162
  * For capabilities that render as their own surface rather than a toolbar
@@ -108,20 +177,24 @@ export class ToolPolicyEngine {
108
177
  * The decision reports whether an assessment was bound, which is the engine's
109
178
  * to answer rather than the policy source's — see
110
179
  * {@link FeaturePolicyDecision.assessmentBound}.
180
+ *
181
+ * `scope` is the surface asking. An item's scope brings in the item's
182
+ * registered settings, as {@link decide} does for the item's toolbar.
111
183
  */
112
- decideFeature(featureId) {
184
+ decideFeature(featureId, scope) {
113
185
  this.assertNotDisposed();
114
186
  const hostDenial = this.hostFeatureGate(featureId);
115
187
  if (hostDenial)
116
188
  return hostDenial;
189
+ const item = this.itemForScope(scope);
117
190
  return interpretFeatureResult(featureId, this.pnpPolicySource.resolveFeature(featureId, {
118
191
  assessment: this.assessment ?? undefined,
119
- currentItemRef: this.currentItemRef ?? undefined,
192
+ item,
120
193
  }),
121
194
  // `resolveFeature` takes the assessment as `undefined` either way, so the
122
195
  // source cannot tell an unbound host from one whose profile is silent.
123
196
  // The engine can.
124
- { assessmentBound: this.assessment !== null });
197
+ { assessmentBound: this.assessment !== null }, resolveToolParameters(featureId, this.assessment, item));
125
198
  }
126
199
  /**
127
200
  * The host gates that hold for a feature id, or `null` when none fires.
@@ -159,17 +232,14 @@ export class ToolPolicyEngine {
159
232
  /**
160
233
  * Apply a partial input update.
161
234
  *
162
- * Each key is value-diffed via `Object.is` before storing, so callers
163
- * may safely re-push the same reference (e.g. on Svelte effect
164
- * re-runs caused by unrelated tracked reads): no-op patches do not
165
- * fire `onPolicyChange` listeners. This contract is relied on by
166
- * `PieAssessmentToolkit.svelte`'s prop-forwarding effect and by the
167
- * coordinator's `decideToolPolicy()` tests.
235
+ * A key that did not change does not fire `onPolicyChange` listeners.
236
+ * `tools` is diffed with `Object.is`. `assessment` is diffed structurally,
237
+ * because hosts rebuild it from their own state on every render; the latest
238
+ * reference is stored either way.
168
239
  *
169
- * `pnpEnforcement` is stored on its own field and always emits with
170
- * a distinct `"pnp-enforcement"` reason when no other key changed,
171
- * so PR 4's auto-on flip can target that reason without a separate
172
- * event channel.
240
+ * `pnpEnforcement` sets the override, `null` or `undefined` returning to
241
+ * auto-mode, and emits with a distinct `"pnp-enforcement"` reason when no
242
+ * other key changed.
173
243
  */
174
244
  updateInputs(patch) {
175
245
  this.assertNotDisposed();
@@ -184,21 +254,17 @@ export class ToolPolicyEngine {
184
254
  }
185
255
  if ("assessment" in patch) {
186
256
  const next = patch.assessment ?? null;
187
- if (!Object.is(this.assessment, next)) {
188
- this.assessment = next;
257
+ const assessmentChanged = !structurallyEqual(this.assessment, next);
258
+ this.assessment = next;
259
+ if (assessmentChanged) {
189
260
  changed = true;
261
+ this.refreshInputDiagnostics();
190
262
  }
191
263
  }
192
- if ("currentItemRef" in patch) {
193
- const next = patch.currentItemRef ?? null;
194
- if (!Object.is(this.currentItemRef, next)) {
195
- this.currentItemRef = next;
196
- changed = true;
197
- }
198
- }
199
- if ("pnpEnforcement" in patch && patch.pnpEnforcement) {
200
- if (this.pnpEnforcement !== patch.pnpEnforcement) {
201
- this.pnpEnforcement = patch.pnpEnforcement;
264
+ if ("pnpEnforcement" in patch) {
265
+ const next = patch.pnpEnforcement ?? null;
266
+ if (this.pnpEnforcementOverride !== next) {
267
+ this.pnpEnforcementOverride = next;
202
268
  pnpChanged = true;
203
269
  }
204
270
  }
@@ -216,9 +282,12 @@ export class ToolPolicyEngine {
216
282
  */
217
283
  replaceToolRegistry(toolRegistry, tools) {
218
284
  this.assertNotDisposed();
285
+ this.unsubscribeRegistry();
219
286
  this.toolRegistry = toolRegistry;
220
287
  this.pnpPolicySource = new PnpPolicySource(toolRegistry);
288
+ this.unsubscribeRegistry = this.watchRegistry();
221
289
  this.tools = tools;
290
+ this.refreshInputDiagnostics();
222
291
  this.emit({ reason: "inputs", inputs: this.snapshotInputs() });
223
292
  }
224
293
  registerPolicySource(source) {
@@ -230,15 +299,15 @@ export class ToolPolicyEngine {
230
299
  });
231
300
  return () => {
232
301
  const idx = this.customSources.indexOf(source);
233
- if (idx !== -1) {
234
- this.customSources.splice(idx, 1);
235
- if (!this.disposed) {
236
- this.emit({
237
- reason: "policy-source-removed",
238
- inputs: this.snapshotInputs(),
239
- });
240
- }
241
- }
302
+ if (idx === -1)
303
+ return;
304
+ this.customSources.splice(idx, 1);
305
+ if (this.disposed)
306
+ return;
307
+ this.emit({
308
+ reason: "policy-source-removed",
309
+ inputs: this.snapshotInputs(),
310
+ });
242
311
  };
243
312
  }
244
313
  onPolicyChange(listener) {
@@ -254,21 +323,99 @@ export class ToolPolicyEngine {
254
323
  if (this.disposed)
255
324
  return;
256
325
  this.disposed = true;
326
+ this.unsubscribeRegistry();
257
327
  this.emit({
258
328
  reason: "disposed",
259
329
  inputs: this.snapshotInputs(),
260
330
  });
261
331
  this.listeners.clear();
332
+ this.itemSettings.clear();
262
333
  this.customSources.length = 0;
263
334
  }
264
335
  snapshotInputs() {
265
336
  return Object.freeze({
266
337
  tools: this.tools,
267
338
  assessment: this.assessment,
268
- currentItemRef: this.currentItemRef,
269
- pnpEnforcement: this.pnpEnforcement,
339
+ pnpEnforcement: this.enforcementFor(),
340
+ pnpEnforcementOverride: this.pnpEnforcementOverride,
341
+ assessmentExpected: !this.assessmentOptional || this.pnpEnforcementOverride === "on",
342
+ diagnostics: this.inputDiagnostics,
270
343
  });
271
344
  }
345
+ /** The override, else auto-mode over the assessment and `item`'s settings. */
346
+ enforcementFor(item) {
347
+ return (this.pnpEnforcementOverride ??
348
+ resolveDefaultPnpEnforcement({
349
+ assessment: this.assessment,
350
+ itemSettings: item?.settings,
351
+ }));
352
+ }
353
+ /** The settings that apply to `itemId`: its newest registration's. */
354
+ itemSettingsFor(itemId) {
355
+ return this.itemSettings.get(itemId)?.at(-1)?.settings;
356
+ }
357
+ emitIfItemSettingsChanged(itemId, before) {
358
+ if (structurallyEqual(before, this.itemSettingsFor(itemId)))
359
+ return;
360
+ this.refreshInputDiagnostics();
361
+ this.emit({ reason: "item-settings", inputs: this.snapshotInputs() });
362
+ }
363
+ /**
364
+ * Recompute {@link ResolvedEngineInputs.diagnostics} from the bound
365
+ * assessment, every mounted item's settings and the registry. Returns
366
+ * whether they changed.
367
+ */
368
+ refreshInputDiagnostics() {
369
+ const items = [];
370
+ for (const id of this.itemSettings.keys()) {
371
+ const settings = this.itemSettingsFor(id);
372
+ if (settings)
373
+ items.push(settings);
374
+ }
375
+ const next = Array.from(this.pnpPolicySource.unknownSupportIds(this.assessment, items), ([supportId, origins]) => unknownSupportIdDiagnostic(supportId, origins));
376
+ if (structurallyEqual(this.inputDiagnostics, next))
377
+ return false;
378
+ this.inputDiagnostics = Object.freeze(next);
379
+ return true;
380
+ }
381
+ /**
382
+ * Registering a tool into the bound registry, or removing one, can make a
383
+ * named id known or unknown, so it emits an `"inputs"` change when the input
384
+ * diagnostics move.
385
+ */
386
+ watchRegistry() {
387
+ return this.toolRegistry.onRegistryChange(() => {
388
+ if (this.disposed)
389
+ return;
390
+ if (this.refreshInputDiagnostics()) {
391
+ this.emit({ reason: "inputs", inputs: this.snapshotInputs() });
392
+ }
393
+ });
394
+ }
395
+ /**
396
+ * The registered item an item scope names, by its canonical id. A scope
397
+ * of any other level names none.
398
+ */
399
+ itemForScope(scope) {
400
+ if (scope?.level !== "item")
401
+ return undefined;
402
+ const id = scope.canonicalItemId || scope.itemId || scope.scopeId;
403
+ const settings = id ? this.itemSettingsFor(id) : undefined;
404
+ return settings ? { id, settings } : undefined;
405
+ }
406
+ /** The registered items whose settings their own toolbar enforces. */
407
+ enforcedItems() {
408
+ const items = [];
409
+ for (const id of this.itemSettings.keys()) {
410
+ const settings = this.itemSettingsFor(id);
411
+ if (!settings)
412
+ continue;
413
+ const item = { id, settings };
414
+ if (this.enforcementFor(item) === "on")
415
+ items.push(item);
416
+ }
417
+ return items;
418
+ }
272
419
  emit(event) {
273
420
  for (const listener of Array.from(this.listeners)) {
274
421
  try {
@@ -11,27 +11,48 @@
11
11
  * The tests in
12
12
  * `tests/policy/compose-decision.test.ts` lock the orchestration in.
13
13
  */
14
- import type { AssessmentEntity, AssessmentItemRef } from "@pie-players/pie-players-shared/types";
15
- import { type CanonicalToolsConfig } from "../../services/tools-config-normalizer.js";
16
- import type { ToolPolicyDecision, ToolPolicyDecisionRequest } from "./decision-types.js";
14
+ import type { AssessmentEntity } from "@pie-players/pie-players-shared/types";
15
+ import { type CanonicalToolsConfig, type ToolPlacementLevel } from "../../services/tools-config-normalizer.js";
16
+ import type { OverrideBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic } from "./decision-types.js";
17
17
  import type { PolicySource } from "./PolicySource.js";
18
- import type { PnpPolicySource } from "../sources/PnpPolicySource.js";
18
+ import { type PnpPolicySourceRule } from "./provenance.js";
19
+ import type { PnpPolicyItem, PnpPolicySource } from "../sources/PnpPolicySource.js";
19
20
  export interface ComposeDecisionInputs {
20
21
  request: ToolPolicyDecisionRequest;
21
22
  tools: CanonicalToolsConfig;
22
23
  pnpPolicy: {
23
24
  source: PnpPolicySource | null;
24
25
  assessment?: AssessmentEntity;
25
- currentItemRef?: AssessmentItemRef;
26
+ /** The item an item-scoped decision is for; see {@link PnpPolicyItem}. */
27
+ item?: PnpPolicyItem;
26
28
  /**
27
29
  * "on" applies the PNP/profile precedence rules; "off" skips step 5
28
- * entirely. Defaults to "on" when an assessment is present
29
- * and the source is non-null (see M8 design Q5).
30
+ * entirely. The engine resolves it from the enforcement override, or
31
+ * from whether the assessment or item carries PNP/profile material.
30
32
  */
31
33
  enforcement: "on" | "off";
32
34
  };
35
+ /**
36
+ * Mounted items whose settings this decision leaves out: set for a section-
37
+ * or assessment-level toolbar. A tool on that toolbar that one of them
38
+ * restricts or requires gets a `tool-policy.itemSettingNotApplied`
39
+ * diagnostic.
40
+ */
41
+ unappliedItems?: readonly PnpPolicyItem[];
33
42
  customSources: readonly PolicySource[];
34
43
  /** Stable identifier used as the provenance `contextId`. */
35
44
  contextId: string;
36
45
  }
37
46
  export declare function composeDecision(inputs: ComposeDecisionInputs): ToolPolicyDecision;
47
+ /**
48
+ * A `tool-policy.overrideBlocked` diagnostic for a tool a `true` test
49
+ * administration override grants and `rule` withdraws. `level` is absent on a
50
+ * feature decision, which has no toolbar level.
51
+ */
52
+ export declare function overrideBlockedDiagnostic(toolId: string, rule: OverrideBlockedDetails["rule"], level?: ToolPlacementLevel): ToolPolicyDiagnostic;
53
+ /**
54
+ * A `tool-policy.unknownSupportId` diagnostic for an id the registry lacks,
55
+ * naming every list that names it. It describes the engine's inputs rather
56
+ * than a decision, so it carries no level.
57
+ */
58
+ export declare function unknownSupportIdDiagnostic(supportId: string, origins: readonly PnpPolicySourceRule[]): ToolPolicyDiagnostic;