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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +451 -541
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +0 -1
  4. package/dist/attempt/TestSession.js +10 -5
  5. package/dist/components/ItemScope.custom-element.js +1 -1
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
  10. package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
  11. package/dist/components/chunks/ItemToolBar-asenkvx6.js +40 -0
  12. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  13. package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
  14. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  15. package/dist/context/assessment-toolkit-context.d.ts +6 -0
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.js +4 -10
  18. package/dist/policy/core/PolicySource.d.ts +1 -1
  19. package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
  20. package/dist/policy/core/ToolPolicyEngine.js +73 -16
  21. package/dist/policy/core/compose-decision.d.ts +3 -3
  22. package/dist/policy/core/compose-decision.js +32 -30
  23. package/dist/policy/core/decision-types.d.ts +51 -34
  24. package/dist/policy/core/feature-decision.d.ts +17 -15
  25. package/dist/policy/core/feature-decision.js +11 -13
  26. package/dist/policy/core/pnp-policy-inputs.d.ts +7 -3
  27. package/dist/policy/core/pnp-policy-inputs.js +17 -33
  28. package/dist/policy/core/provenance.d.ts +2 -2
  29. package/dist/policy/core/tool-parameters.d.ts +12 -0
  30. package/dist/policy/core/tool-parameters.js +27 -0
  31. package/dist/policy/engine.d.ts +1 -1
  32. package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
  33. package/dist/policy/sources/PnpPolicySource.js +57 -29
  34. package/dist/runtime/SectionControllerBinding.d.ts +6 -2
  35. package/dist/runtime/SectionControllerBinding.js +21 -15
  36. package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
  37. package/dist/runtime/adapter/subscriber-fanout.js +1 -4
  38. package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
  39. package/dist/runtime/composition-emit-scheduler.js +1 -1
  40. package/dist/runtime/content-region.d.ts +0 -4
  41. package/dist/runtime/content-region.js +2 -2
  42. package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
  43. package/dist/runtime/core/SectionEngineCore.js +0 -4
  44. package/dist/runtime/core/cohort.d.ts +0 -5
  45. package/dist/runtime/core/cohort.js +0 -9
  46. package/dist/runtime/core/engine-output.d.ts +3 -2
  47. package/dist/runtime/core/engine-transition.js +3 -2
  48. package/dist/runtime/registration-events.d.ts +11 -14
  49. package/dist/runtime/runtime-id.d.ts +1 -1
  50. package/dist/runtime/runtime-id.js +1 -1
  51. package/dist/runtime/shell-scope.d.ts +4 -4
  52. package/dist/runtime/shell-scope.js +15 -9
  53. package/dist/runtime/tool-host-contract.d.ts +0 -4
  54. package/dist/runtime/tool-host-contract.js +1 -1
  55. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  56. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  57. package/dist/services/ElementToolStateStore.d.ts +32 -61
  58. package/dist/services/ElementToolStateStore.js +37 -67
  59. package/dist/services/HighlightCoordinator.d.ts +9 -21
  60. package/dist/services/HighlightCoordinator.js +52 -80
  61. package/dist/services/RangeSerializer.d.ts +4 -35
  62. package/dist/services/RangeSerializer.js +4 -48
  63. package/dist/services/SSMLExtractor.d.ts +8 -31
  64. package/dist/services/SSMLExtractor.js +18 -40
  65. package/dist/services/TTSService.d.ts +51 -57
  66. package/dist/services/TTSService.js +323 -284
  67. package/dist/services/ToolCoordinator.d.ts +6 -49
  68. package/dist/services/ToolCoordinator.js +25 -61
  69. package/dist/services/ToolRegistry.d.ts +83 -80
  70. package/dist/services/ToolRegistry.js +91 -67
  71. package/dist/services/ToolkitCoordinator.d.ts +83 -81
  72. package/dist/services/ToolkitCoordinator.js +186 -174
  73. package/dist/services/framework-error.d.ts +30 -7
  74. package/dist/services/framework-error.js +10 -13
  75. package/dist/services/interfaces.d.ts +51 -71
  76. package/dist/services/section-controller-types.d.ts +7 -7
  77. package/dist/services/section-session-assignment.js +6 -8
  78. package/dist/services/spoken-audio-cards.d.ts +2 -4
  79. package/dist/services/spoken-audio-cards.js +2 -4
  80. package/dist/services/tool-config-validation.d.ts +5 -7
  81. package/dist/services/tool-config-validation.js +11 -26
  82. package/dist/services/tool-context.d.ts +39 -24
  83. package/dist/services/tool-context.js +158 -92
  84. package/dist/services/tool-failure.d.ts +30 -0
  85. package/dist/services/tool-failure.js +11 -0
  86. package/dist/services/tool-icons.d.ts +1 -2
  87. package/dist/services/tool-icons.js +2 -2
  88. package/dist/services/tool-instance-id.d.ts +8 -6
  89. package/dist/services/tool-instance-id.js +12 -18
  90. package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
  91. package/dist/services/tool-providers/TTSToolProvider.js +7 -63
  92. package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
  93. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
  94. package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
  95. package/dist/services/tool-request.d.ts +4 -0
  96. package/dist/services/tool-request.js +7 -2
  97. package/dist/services/toolbar-decided-tools.d.ts +2 -0
  98. package/dist/services/toolbar-decided-tools.js +2 -0
  99. package/dist/services/toolbar-items.js +2 -5
  100. package/dist/services/tools-config-normalizer.d.ts +18 -9
  101. package/dist/services/tools-config-normalizer.js +25 -19
  102. package/dist/services/tts/browser-provider.d.ts +2 -5
  103. package/dist/services/tts/browser-provider.js +13 -22
  104. package/dist/services/tts/catalog-span-alignment.js +14 -7
  105. package/dist/services/tts/flat-tree.d.ts +0 -7
  106. package/dist/services/tts/flat-tree.js +2 -2
  107. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  108. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  109. package/dist/services/tts/generated-speech/index.js +1 -1
  110. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  111. package/dist/services/tts/generated-speech/types.js +1 -1
  112. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
  113. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  114. package/dist/services/tts/math-aware-text-processing.js +10 -29
  115. package/dist/services/tts/math-speech.d.ts +1 -1
  116. package/dist/services/tts/math-speech.js +3 -0
  117. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  118. package/dist/services/tts/mathml-sanitization.js +9 -1
  119. package/dist/services/tts/provider-options.d.ts +12 -1
  120. package/dist/services/tts/ssml/spoken-text.js +1 -1
  121. package/dist/services/tts/start-failure.d.ts +0 -7
  122. package/dist/services/tts/start-failure.js +1 -1
  123. package/dist/services/tts/text-segmentation.d.ts +5 -0
  124. package/dist/services/tts/text-segmentation.js +13 -10
  125. package/dist/services/tts-runtime-config.d.ts +32 -12
  126. package/dist/services/tts-runtime-config.js +14 -20
  127. package/dist/tools/content-capability-resolution.d.ts +2 -2
  128. package/dist/tools/registration.d.ts +7 -6
  129. package/dist/tools/registration.js +10 -9
  130. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  131. package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
  132. package/dist/tools/tool-surface-host.js +1 -8
  133. package/package.json +8 -6
  134. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
  135. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
  136. package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
  137. package/dist/services/ContextVariableStore.d.ts +0 -113
  138. package/dist/services/ContextVariableStore.js +0 -206
  139. package/dist/services/tts-control-events.d.ts +0 -13
  140. package/dist/services/tts-control-events.js +0 -7
  141. package/dist/utils/logger.d.ts +0 -6
  142. package/dist/utils/logger.js +0 -10
package/dist/index.d.ts CHANGED
@@ -16,38 +16,38 @@ export { connectToolRuntimeContext, dispatchCrossBoundaryEvent, } from "./runtim
16
16
  export type { AccessibilityCatalogResolverApi, ElementToolStateStoreApi, HighlightCoordinatorApi, ToolCoordinatorApi, ToolkitCoordinatorApi, SpeakOptions, TtsServiceApi, ToolState, ToolStateFilter, } from "./services/interfaces.js";
17
17
  export type { CatalogCardForm, CatalogChangeEvent, CatalogChangeListener, CatalogChangeReason, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerCard, CatalogOwnerRegistration, CatalogOwnerSnapshot, CatalogOwnerView, CatalogStatistics, CatalogType, ResolvedCatalog, } from "./services/AccessibilityCatalogResolver.js";
18
18
  export { AccessibilityCatalogResolver } from "./services/AccessibilityCatalogResolver.js";
19
- export { ElementToolStateStore } from "./services/ElementToolStateStore.js";
19
+ export { type ElementIdComponents, ElementToolStateStore, } from "./services/ElementToolStateStore.js";
20
20
  export type { Annotation } from "./services/HighlightCoordinator.js";
21
21
  export { HighlightColor, HighlightCoordinator, HighlightType, } from "./services/HighlightCoordinator.js";
22
22
  export type { SerializedRange } from "./services/RangeSerializer.js";
23
23
  export type { I18nProvider, InterpolationValues, LocaleCode, MessageKeyInput, PluralOptions, TextDirection, } from "@pie-players/pie-players-shared/i18n";
24
- export type { ResolvedToolContext, ToolbarContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap, ToolContextResolverResult, ToolActivation, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolRegistryChangeEvent, ToolRegistryChangeKind, ToolRegistryChangeListener, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolRegistration, } from "./services/ToolRegistry.js";
24
+ export type { ResolvedToolContext, ToolbarContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap, ToolContextResolverResult, ToolActivation, ToolCallbackFailureHandler, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolRegistryChangeEvent, ToolRegistryChangeKind, ToolRegistryChangeListener, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolRegistration, } from "./services/ToolRegistry.js";
25
25
  export { ToolRegistry } from "./services/ToolRegistry.js";
26
26
  export type { ToolOpenRequest, ToolRequestTarget, } from "./services/tool-request.js";
27
27
  export type { AssessmentToolContext, BaseToolContext, ElementToolContext, ItemToolContext, PassageToolContext, RubricToolContext, SectionToolContext, ToolContext, ToolLevel, } from "./services/tool-context.js";
28
- export { hasReadableText } from "./services/tool-context.js";
28
+ export { hasReadableText, hasSpokenContent, } from "./services/tool-context.js";
29
29
  export type { CreateToolsConfigArgs } from "./services/create-tools-config.js";
30
30
  export { createToolsConfig } from "./services/create-tools-config.js";
31
31
  export type { ToolComponentFactory, ToolComponentFactoryMap, ToolComponentOverrides, ToolTagMap, } from "./tools/tool-tag-map.js";
32
32
  export type { ExtractionResult } from "./services/SSMLExtractor.js";
33
33
  export { SSMLExtractor } from "./services/SSMLExtractor.js";
34
- export { ToolCoordinator, ZIndexLayer } from "./services/ToolCoordinator.js";
34
+ export { ZIndexLayer } from "./services/ToolCoordinator.js";
35
35
  export type { ProviderLifecycleContext, SectionControllerContext, SectionControllerEvent, SectionControllerEventType, SectionItemEvent, SectionItemEventType, SectionControllerFactoryDefaults, SectionControllerHandle, SectionControllerKey, SectionScopedEvent, SectionScopedEventType, SectionSessionPersistenceConfig, SectionItemEventSubscriptionArgs, SectionScopedEventSubscriptionArgs, SectionEventSubscriptionArgs, SectionSessionPersistenceStrategy, SectionControllerLoadedRenderable, SectionControllerRuntimeState, SectionControllerSessionState, SectionPersistenceFactoryDefaults, ToolkitCoordinatorConfig, ToolkitCoordinatorHooks, ToolkitErrorContext, } from "./services/ToolkitCoordinator.js";
36
36
  export { ToolkitCoordinator } from "./services/ToolkitCoordinator.js";
37
37
  export type { CalculatorToolProviderConfig, CanonicalToolsConfig, ToolPlacementConfig, ToolPlacementLevel, ToolPolicyConfig, ToolProvidersConfig, ToolsConfigInput, TextToSpeechToolProviderConfig, } from "./services/tools-config-normalizer.js";
38
- export type { FrameworkErrorKind, FrameworkErrorModel, FrameworkErrorScope, FrameworkErrorSeverity, } from "./services/framework-error.js";
38
+ export type { FrameworkErrorCohort, FrameworkErrorKind, FrameworkErrorModel, FrameworkErrorScope, FrameworkErrorSeverity, } from "./services/framework-error.js";
39
39
  export { frameworkErrorFromUnknown, toFrameworkErrorModel, } from "./services/framework-error.js";
40
40
  export type { FrameworkErrorListener } from "./services/framework-error-bus.js";
41
+ export type { ToolFailurePhase } from "./services/tool-failure.js";
41
42
  export type { ToolConfigDiagnostic, ToolConfigDiagnosticSeverity, ToolConfigStrictness, ToolConfigValidationResult, } from "./services/tool-config-validation.js";
42
43
  export type { ToolbarButtonItem, ToolbarItem, ToolbarItemBase, ToolbarLinkItem, } from "./services/toolbar-items.js";
43
44
  export type { ToolScopeLevel } from "./services/tool-instance-id.js";
44
45
  export { createScopedToolId } from "./services/tool-instance-id.js";
45
- export type { TTSConfig } from "./services/TTSService.js";
46
46
  export { PlaybackState, TTSService } from "./services/TTSService.js";
47
47
  export { BrowserTTSProvider } from "./services/tts/browser-provider.js";
48
48
  export type { SREMathSpeechOptions } from "./services/tts/math-speech.js";
49
49
  export type { TTSLayoutMode, TTSRuntimeSettings, TTSSpeedOption, TTSSpeedOptionConfig, } from "./services/tts-runtime-config.js";
50
- export type { ITTSProvider, ITTSProviderImplementation, TTSSpeechSegment, TTSProviderCapabilities, } from "@pie-players/pie-tts";
50
+ export type { ITTSProvider, ITTSProviderImplementation, TTSConfig, TTSSpeechSegment, TTSProviderCapabilities, } from "@pie-players/pie-tts";
51
51
  export type { TestAttemptItemSession, TestAttemptSession, TestAttemptSessionNavigationState, TestAttemptSessionRealization, } from "./attempt/TestSession.js";
52
52
  export { createNewTestAttemptSession, setCurrentPosition, toItemSessionsRecord, upsertItemSessionFromPieSessionChange, upsertVisitedItem, } from "./attempt/TestSession.js";
53
53
  export type { AssessmentSession, AssessmentSectionSessionState, AssessmentSessionNavigationState, AssessmentSessionRealization, } from "./attempt/AssessmentSession.js";
package/dist/index.js CHANGED
@@ -15,25 +15,19 @@ export { createShellEventBridge, } from "./runtime/shell-event-bridge.js";
15
15
  export { connectToolRuntimeContext, dispatchCrossBoundaryEvent, } from "./runtime/tool-host-contract.js";
16
16
  export { AccessibilityCatalogResolver } from "./services/AccessibilityCatalogResolver.js";
17
17
  // Element Tool State Store (Element-level ephemeral tool state)
18
- export { ElementToolStateStore } from "./services/ElementToolStateStore.js";
18
+ export { ElementToolStateStore, } from "./services/ElementToolStateStore.js";
19
19
  export { HighlightColor, HighlightCoordinator, HighlightType, } from "./services/HighlightCoordinator.js";
20
20
  export { ToolRegistry } from "./services/ToolRegistry.js";
21
- export { hasReadableText } from "./services/tool-context.js";
21
+ export { hasReadableText, hasSpokenContent, } from "./services/tool-context.js";
22
22
  export { createToolsConfig } from "./services/create-tools-config.js";
23
23
  export { SSMLExtractor } from "./services/SSMLExtractor.js";
24
24
  // Tool Coordinator
25
- export { ToolCoordinator, ZIndexLayer } from "./services/ToolCoordinator.js";
25
+ export { ZIndexLayer } from "./services/ToolCoordinator.js";
26
26
  export { ToolkitCoordinator } from "./services/ToolkitCoordinator.js";
27
27
  export { frameworkErrorFromUnknown, toFrameworkErrorModel, } from "./services/framework-error.js";
28
28
  export { createScopedToolId } from "./services/tool-instance-id.js";
29
+ // Text-to-Speech Service
29
30
  export { PlaybackState, TTSService } from "./services/TTSService.js";
30
31
  export { BrowserTTSProvider } from "./services/tts/browser-provider.js";
31
32
  export { createNewTestAttemptSession, setCurrentPosition, toItemSessionsRecord, upsertItemSessionFromPieSessionChange, upsertVisitedItem, } from "./attempt/TestSession.js";
32
33
  export { createNewAssessmentSession, setCurrentSectionPosition, upsertSectionSession, } from "./attempt/AssessmentSession.js";
33
- // Section Player - Use @pie-players/pie-section-player web component
34
- // ============================================================================
35
- // Shared Components
36
- // ============================================================================
37
- // ItemToolBar custom element registration helper is exported via package.json exports field
38
- // Import using: import '@pie-players/pie-assessment-toolkit/components/item-toolbar-element';
39
- // PieAssessmentToolkit custom element registration helper is exported via package.json exports field
@@ -36,7 +36,7 @@ export interface PolicySourceProvenanceEntry {
36
36
  * Custom precedence value used for the decision log. Defaults to
37
37
  * `100 + sourceIndex` (engine-assigned) when `undefined`. Sources
38
38
  * MAY override only if they know what they are doing — overriding
39
- * to a value `<= 6` lets the source masquerade as a PNP/profile rule and
39
+ * to a value `<= 8` lets the source masquerade as a PNP/profile rule and
40
40
  * is generally a bug.
41
41
  */
42
42
  precedence?: number;
@@ -14,13 +14,13 @@
14
14
  * connectedCallback. `ToolkitCoordinator` owns the instance, and
15
15
  * `<pie-item-toolbar>` reads its decisions through the coordinator.
16
16
  */
17
- import type { AssessmentEntity, ItemSettings } from "@pie-players/pie-players-shared/types";
18
- import type { CanonicalToolsConfig, ToolPlacementLevel } from "../../services/tools-config-normalizer.js";
17
+ import type { AssessmentEntity, ItemSettings, ToolParametersFor } from "@pie-players/pie-players-shared/types";
18
+ import type { CanonicalToolsConfig, PnpEnforcementMode, ToolPlacementLevel } from "../../services/tools-config-normalizer.js";
19
19
  import type { ToolRegistry } from "../../services/ToolRegistry.js";
20
- import type { ToolPolicyDecision, ToolPolicyDecisionRequest, ToolScope } from "./decision-types.js";
20
+ import type { ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolScope } from "./decision-types.js";
21
21
  import type { PolicySource } from "./PolicySource.js";
22
22
  import type { FeaturePolicyDecision } from "./feature-decision.js";
23
- export type PnpEnforcementMode = "on" | "off";
23
+ export type { PnpEnforcementMode };
24
24
  export interface ToolPolicyEngineInputs {
25
25
  tools?: CanonicalToolsConfig | null;
26
26
  assessment?: AssessmentEntity | null;
@@ -29,7 +29,7 @@ export interface ToolPolicyEngineInputs {
29
29
  * resolves per decision through {@link resolveDefaultPnpEnforcement}: `"on"`
30
30
  * when the bound `assessment` carries PNP/profile policy material (PNP,
31
31
  * district policy, test administration), and for a decision scoped to an item
32
- * also when that item's settings carry required/restricted/parameters;
32
+ * also when that item's settings require or restrict a tool;
33
33
  * `"off"` otherwise. `tests/policy/pnp-default-on.test.ts` locks the rule.
34
34
  *
35
35
  * Hosts that want to force a mode pass `"on"` or `"off"`
@@ -44,6 +44,12 @@ export interface ToolPolicyEngineArgs {
44
44
  customSources?: readonly PolicySource[];
45
45
  /** Stable context label used in provenance trails. Defaults to "tool-policy". */
46
46
  contextId?: string;
47
+ /**
48
+ * Whether the host may leave the assessment unbound without that being a
49
+ * misconfiguration; see {@link ResolvedEngineInputs.assessmentExpected}.
50
+ * Defaults to `false`.
51
+ */
52
+ assessmentOptional?: boolean;
47
53
  }
48
54
  export interface ToolPolicyChangeEvent {
49
55
  /**
@@ -65,11 +71,26 @@ export interface ResolvedEngineInputs {
65
71
  pnpEnforcement: PnpEnforcementMode;
66
72
  /** The host's override of auto-mode, or `null` in auto-mode. */
67
73
  pnpEnforcementOverride: PnpEnforcementMode | null;
74
+ /**
75
+ * Whether an unbound assessment is a misconfiguration: always, unless the
76
+ * engine was built `assessmentOptional`, and then only while enforcement is
77
+ * overridden `"on"`.
78
+ */
79
+ assessmentExpected: boolean;
80
+ /**
81
+ * Conflicts in the inputs themselves, independent of any decision: a
82
+ * `tool-policy.unknownSupportId` for each id the assessment or a mounted
83
+ * item's settings name that no registered tool carries. Recomputed when the
84
+ * assessment, an item's settings or the registry's tools change.
85
+ */
86
+ diagnostics: readonly ToolPolicyDiagnostic[];
68
87
  }
69
88
  export type ToolPolicyChangeListener = (event: ToolPolicyChangeEvent) => void;
70
89
  export declare class ToolPolicyEngine {
71
90
  private toolRegistry;
91
+ private unsubscribeRegistry;
72
92
  private readonly contextId;
93
+ private readonly assessmentOptional;
73
94
  private pnpPolicySource;
74
95
  private readonly customSources;
75
96
  private readonly listeners;
@@ -81,6 +102,7 @@ export declare class ToolPolicyEngine {
81
102
  * see {@link registerItemSettings}.
82
103
  */
83
104
  private readonly itemSettings;
105
+ private inputDiagnostics;
84
106
  private disposed;
85
107
  constructor(args: ToolPolicyEngineArgs);
86
108
  /**
@@ -109,8 +131,13 @@ export declare class ToolPolicyEngine {
109
131
  * Whether some surface is granted `featureId`: a decision with no item scope,
110
132
  * or one scoped to a registered item. A provider serves every surface, so its
111
133
  * failure denies the accommodation wherever one surface is granted it.
134
+ *
135
+ * `enforced` counts only scopes whose `pnpEnforcement` is on, as a toolbar
136
+ * tool's decisions do; a feature with no placement leaves it unset.
112
137
  */
113
- grantsFeatureAnywhere(featureId: string): boolean;
138
+ grantsFeatureAnywhere(featureId: string, options?: {
139
+ enforced?: boolean;
140
+ }): boolean;
114
141
  /**
115
142
  * File a mounted item's settings under its canonical id. They govern the
116
143
  * decisions scoped to that item: its own toolbar, and a feature its content
@@ -148,7 +175,7 @@ export declare class ToolPolicyEngine {
148
175
  * `scope` is the surface asking. An item's scope brings in the item's
149
176
  * registered settings, as {@link decide} does for the item's toolbar.
150
177
  */
151
- decideFeature(featureId: string, scope?: ToolScope): FeaturePolicyDecision;
178
+ decideFeature<K extends string>(featureId: K, scope?: ToolScope): FeaturePolicyDecision<ToolParametersFor<K>>;
152
179
  /**
153
180
  * The host gates that hold for a feature id, or `null` when none fires.
154
181
  *
@@ -195,6 +222,18 @@ export declare class ToolPolicyEngine {
195
222
  /** The settings that apply to `itemId`: its newest registration's. */
196
223
  private itemSettingsFor;
197
224
  private emitIfItemSettingsChanged;
225
+ /**
226
+ * Recompute {@link ResolvedEngineInputs.diagnostics} from the bound
227
+ * assessment, every mounted item's settings and the registry. Returns
228
+ * whether they changed.
229
+ */
230
+ private refreshInputDiagnostics;
231
+ /**
232
+ * Registering a tool into the bound registry, or removing one, can make a
233
+ * named id known or unknown, so it emits an `"inputs"` change when the input
234
+ * diagnostics move.
235
+ */
236
+ private watchRegistry;
198
237
  /**
199
238
  * The registered item an item scope names, by its canonical id. A scope
200
239
  * of any other level names none.
@@ -16,8 +16,9 @@
16
16
  */
17
17
  import { normalizeToolList, normalizeToolsConfig, } from "../../services/tools-config-normalizer.js";
18
18
  import { hostFeatureDenial, interpretFeatureResult, } from "./feature-decision.js";
19
- import { composeDecision } from "./compose-decision.js";
19
+ import { composeDecision, unknownSupportIdDiagnostic, } from "./compose-decision.js";
20
20
  import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
21
+ import { resolveToolParameters } from "./tool-parameters.js";
21
22
  import { structurallyEqual } from "../../utils/structural-equality.js";
22
23
  import { PnpPolicySource, } from "../sources/PnpPolicySource.js";
23
24
  const DEFAULT_TOOLS = normalizeToolsConfig({
@@ -27,7 +28,9 @@ const DEFAULT_TOOLS = normalizeToolsConfig({
27
28
  });
28
29
  export class ToolPolicyEngine {
29
30
  toolRegistry;
31
+ unsubscribeRegistry;
30
32
  contextId;
33
+ assessmentOptional;
31
34
  pnpPolicySource;
32
35
  customSources;
33
36
  listeners = new Set();
@@ -39,16 +42,20 @@ export class ToolPolicyEngine {
39
42
  * see {@link registerItemSettings}.
40
43
  */
41
44
  itemSettings = new Map();
45
+ inputDiagnostics = Object.freeze([]);
42
46
  disposed = false;
43
47
  constructor(args) {
44
48
  this.toolRegistry = args.toolRegistry;
45
49
  this.contextId = args.contextId ?? "tool-policy";
50
+ this.assessmentOptional = args.assessmentOptional === true;
46
51
  this.pnpPolicySource = new PnpPolicySource(this.toolRegistry);
47
52
  this.customSources = args.customSources ? [...args.customSources] : [];
48
53
  const inputs = args.inputs ?? {};
49
54
  this.tools = inputs.tools ?? DEFAULT_TOOLS;
50
55
  this.assessment = inputs.assessment ?? null;
51
56
  this.pnpEnforcementOverride = inputs.pnpEnforcement ?? null;
57
+ this.refreshInputDiagnostics();
58
+ this.unsubscribeRegistry = this.watchRegistry();
52
59
  }
53
60
  /**
54
61
  * Resolve the visible tool set for a given placement level and
@@ -98,11 +105,18 @@ export class ToolPolicyEngine {
98
105
  * Whether some surface is granted `featureId`: a decision with no item scope,
99
106
  * or one scoped to a registered item. A provider serves every surface, so its
100
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.
101
111
  */
102
- grantsFeatureAnywhere(featureId) {
103
- if (this.decideFeature(featureId).granted)
112
+ grantsFeatureAnywhere(featureId, options = {}) {
113
+ const counts = (item) => !options.enforced || this.enforcementFor(item) === "on";
114
+ if (counts() && this.decideFeature(featureId).granted)
104
115
  return true;
105
116
  for (const id of this.itemSettings.keys()) {
117
+ const settings = this.itemSettingsFor(id);
118
+ if (!counts(settings ? { id, settings } : undefined))
119
+ continue;
106
120
  if (this.decideFeature(featureId, { level: "item", scopeId: id }).granted) {
107
121
  return true;
108
122
  }
@@ -172,14 +186,15 @@ export class ToolPolicyEngine {
172
186
  const hostDenial = this.hostFeatureGate(featureId);
173
187
  if (hostDenial)
174
188
  return hostDenial;
189
+ const item = this.itemForScope(scope);
175
190
  return interpretFeatureResult(featureId, this.pnpPolicySource.resolveFeature(featureId, {
176
191
  assessment: this.assessment ?? undefined,
177
- item: this.itemForScope(scope),
192
+ item,
178
193
  }),
179
194
  // `resolveFeature` takes the assessment as `undefined` either way, so the
180
195
  // source cannot tell an unbound host from one whose profile is silent.
181
196
  // The engine can.
182
- { assessmentBound: this.assessment !== null });
197
+ { assessmentBound: this.assessment !== null }, resolveToolParameters(featureId, this.assessment, item));
183
198
  }
184
199
  /**
185
200
  * The host gates that hold for a feature id, or `null` when none fires.
@@ -239,9 +254,12 @@ export class ToolPolicyEngine {
239
254
  }
240
255
  if ("assessment" in patch) {
241
256
  const next = patch.assessment ?? null;
242
- if (!structurallyEqual(this.assessment, next))
243
- changed = true;
257
+ const assessmentChanged = !structurallyEqual(this.assessment, next);
244
258
  this.assessment = next;
259
+ if (assessmentChanged) {
260
+ changed = true;
261
+ this.refreshInputDiagnostics();
262
+ }
245
263
  }
246
264
  if ("pnpEnforcement" in patch) {
247
265
  const next = patch.pnpEnforcement ?? null;
@@ -264,9 +282,12 @@ export class ToolPolicyEngine {
264
282
  */
265
283
  replaceToolRegistry(toolRegistry, tools) {
266
284
  this.assertNotDisposed();
285
+ this.unsubscribeRegistry();
267
286
  this.toolRegistry = toolRegistry;
268
287
  this.pnpPolicySource = new PnpPolicySource(toolRegistry);
288
+ this.unsubscribeRegistry = this.watchRegistry();
269
289
  this.tools = tools;
290
+ this.refreshInputDiagnostics();
270
291
  this.emit({ reason: "inputs", inputs: this.snapshotInputs() });
271
292
  }
272
293
  registerPolicySource(source) {
@@ -278,15 +299,15 @@ export class ToolPolicyEngine {
278
299
  });
279
300
  return () => {
280
301
  const idx = this.customSources.indexOf(source);
281
- if (idx !== -1) {
282
- this.customSources.splice(idx, 1);
283
- if (!this.disposed) {
284
- this.emit({
285
- reason: "policy-source-removed",
286
- inputs: this.snapshotInputs(),
287
- });
288
- }
289
- }
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
+ });
290
311
  };
291
312
  }
292
313
  onPolicyChange(listener) {
@@ -302,6 +323,7 @@ export class ToolPolicyEngine {
302
323
  if (this.disposed)
303
324
  return;
304
325
  this.disposed = true;
326
+ this.unsubscribeRegistry();
305
327
  this.emit({
306
328
  reason: "disposed",
307
329
  inputs: this.snapshotInputs(),
@@ -316,6 +338,8 @@ export class ToolPolicyEngine {
316
338
  assessment: this.assessment,
317
339
  pnpEnforcement: this.enforcementFor(),
318
340
  pnpEnforcementOverride: this.pnpEnforcementOverride,
341
+ assessmentExpected: !this.assessmentOptional || this.pnpEnforcementOverride === "on",
342
+ diagnostics: this.inputDiagnostics,
319
343
  });
320
344
  }
321
345
  /** The override, else auto-mode over the assessment and `item`'s settings. */
@@ -333,8 +357,41 @@ export class ToolPolicyEngine {
333
357
  emitIfItemSettingsChanged(itemId, before) {
334
358
  if (structurallyEqual(before, this.itemSettingsFor(itemId)))
335
359
  return;
360
+ this.refreshInputDiagnostics();
336
361
  this.emit({ reason: "item-settings", inputs: this.snapshotInputs() });
337
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
+ }
338
395
  /**
339
396
  * The registered item an item scope names, by its canonical id. A scope
340
397
  * of any other level names none.
@@ -52,7 +52,7 @@ export declare function composeDecision(inputs: ComposeDecisionInputs): ToolPoli
52
52
  export declare function overrideBlockedDiagnostic(toolId: string, rule: OverrideBlockedDetails["rule"], level?: ToolPlacementLevel): ToolPolicyDiagnostic;
53
53
  /**
54
54
  * A `tool-policy.unknownSupportId` diagnostic for an id the registry lacks,
55
- * attributed to the highest-precedence rule naming it. `level` is absent on a
56
- * feature decision, which has no toolbar level.
55
+ * naming every list that names it. It describes the engine's inputs rather
56
+ * than a decision, so it carries no level.
57
57
  */
58
- export declare function unknownSupportIdDiagnostic(supportId: string, origins: readonly PnpPolicySourceRule[], level?: ToolPlacementLevel): ToolPolicyDiagnostic;
58
+ export declare function unknownSupportIdDiagnostic(supportId: string, origins: readonly PnpPolicySourceRule[]): ToolPolicyDiagnostic;
@@ -12,6 +12,7 @@
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 { resolveToolParameters } from "./tool-parameters.js";
15
16
  import { ToolPolicyProvenanceBuilder, } from "./provenance.js";
16
17
  export function composeDecision(inputs) {
17
18
  const { request, tools, pnpPolicy, customSources, contextId } = inputs;
@@ -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) => {
@@ -134,19 +135,12 @@ export function composeDecision(inputs) {
134
135
  value: decision.value,
135
136
  });
136
137
  }
137
- // An id no tool is registered under matches nothing placed, so whatever it
138
- // names is absent with no trace of why. Reported per decision because the
139
- // ids arrive with the profile and settings, after the tools config.
140
- for (const [supportId, origins] of pnpPolicyResult.unmappedSupportIds) {
141
- diagnostics.push(unknownSupportIdDiagnostic(supportId, origins, request.level));
142
- }
143
138
  for (const [toolId, rule] of pnpPolicyResult.blockedOverrides) {
144
139
  diagnostics.push(overrideBlockedDiagnostic(toolId, rule, request.level));
145
140
  }
146
141
  // 5a — remove PNP/profile-blocked tools from the candidate set.
147
- candidates = candidates.filter((toolId) => {
148
- return !pnpPolicyResult.blockedToolIds.has(toolId);
149
- });
142
+ const { blockedToolIds } = pnpPolicyResult;
143
+ candidates = candidates.filter((toolId) => !blockedToolIds.has(toolId));
150
144
  // 5b — surface requiredToolBlocked diagnostics ONLY for
151
145
  // PNP/profile-mandated tools that the host removed (i.e., the tool was
152
146
  // NOT in `postHostCandidates`). Mandated tools removed by
@@ -154,20 +148,24 @@ export function composeDecision(inputs) {
154
148
  // blocking a `requiredTools` mandate) are an internal policy
155
149
  // resolution and must not be reported as a host conflict — the
156
150
  // loser's `block` decision is already in the trail with full
157
- // 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.
158
153
  //
159
154
  // `details` carries `{ rule, hostRule, hostValue }` so consumers
160
155
  // can render the conflict as a single sentence without
161
156
  // re-deriving the host gate from `provenance.features`. See
162
157
  // `RequiredToolBlockedDetails` for the typed shape.
163
- for (const mandatedToolId of pnpPolicyResult.mandatedToolIds) {
164
- const removedByHost = !postHostCandidates.has(mandatedToolId);
165
- 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))
161
+ continue;
162
+ if (!placement.includes(mandatedToolId) &&
163
+ placedAtAnyLevel.has(mandatedToolId)) {
166
164
  continue;
167
- const flag = pnpPolicyResult.perToolFlags.get(mandatedToolId);
168
- const ruleName = flag?.rule ?? "required-tool";
165
+ }
169
166
  const hostGate = detectHostRemovalGate(mandatedToolId, {
170
- placement,
167
+ placedAtAnyLevel,
168
+ allTools: tools.placement,
171
169
  allowed,
172
170
  blocked,
173
171
  providers: tools.providers,
@@ -181,7 +179,9 @@ export function composeDecision(inputs) {
181
179
  code: "tool-policy.requiredToolBlocked",
182
180
  level: request.level,
183
181
  toolId: mandatedToolId,
184
- 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}.`,
185
185
  details,
186
186
  });
187
187
  builder.addDecision({
@@ -288,7 +288,8 @@ export function composeDecision(inputs) {
288
288
  toolId,
289
289
  required: flag?.required ?? false,
290
290
  alwaysAvailable: flag?.alwaysAvailable ?? false,
291
- settings: flag?.settings,
291
+ activateAtInit: flag?.activateAtInit ?? false,
292
+ parameters: resolveToolParameters(toolId, pnpPolicy.assessment, pnpPolicy.item),
292
293
  };
293
294
  });
294
295
  builder.reconcileFinalStates(survivingIds);
@@ -305,10 +306,11 @@ export function composeDecision(inputs) {
305
306
  * (blocklist) last — so the FIRST gate that would have matched wins,
306
307
  * which is the gate the pipeline actually invoked. Subsequent gates
307
308
  * never get to evaluate a tool that an earlier step already removed.
309
+ * Step 5b passes only a tool this level places or no level places.
308
310
  */
309
311
  function detectHostRemovalGate(toolId, args) {
310
- if (!args.placement.includes(toolId)) {
311
- return { hostRule: "placement-missing", hostValue: args.placement };
312
+ if (!args.placedAtAnyLevel.has(toolId)) {
313
+ return { hostRule: "placement-missing", hostValue: args.allTools };
312
314
  }
313
315
  if (args.providers?.[toolId]?.enabled === false) {
314
316
  return { hostRule: "provider-disabled", hostValue: false };
@@ -322,11 +324,11 @@ function detectHostRemovalGate(toolId, args) {
322
324
  hostValue: Array.from(args.blocked),
323
325
  };
324
326
  }
325
- // Defensive: `removedByHost` is true at the call site (the tool is
326
- // in `mandatedToolIds` but not in `postHostCandidates`), so one of
327
- // the four gates above MUST have fired. If none did, the host
328
- // pipeline has a bug; surface a best-effort rather than throwing.
329
- return { hostRule: "placement-missing", hostValue: args.placement };
327
+ // Defensive: at the call site the tool is in `mandates` but not in
328
+ // `postHostCandidates`, so one of the four gates above MUST have
329
+ // fired. If none did, the host pipeline has a bug; surface a
330
+ // best-effort rather than throwing.
331
+ return { hostRule: "placement-missing", hostValue: args.allTools };
330
332
  }
331
333
  /** Where each PNP/profile rule's ids are authored. */
332
334
  const PNP_RULE_FIELDS = {
@@ -337,6 +339,7 @@ const PNP_RULE_FIELDS = {
337
339
  "item-requirement": "item settings.requiredTools",
338
340
  "district-requirement": "settings.districtPolicy.requiredTools",
339
341
  "pnp-support": "personalNeedsProfile.supports",
342
+ "pnp-activate-at-init": "personalNeedsProfile.activateAtInit",
340
343
  };
341
344
  /**
342
345
  * A `tool-policy.overrideBlocked` diagnostic for a tool a `true` test
@@ -354,14 +357,13 @@ export function overrideBlockedDiagnostic(toolId, rule, level) {
354
357
  }
355
358
  /**
356
359
  * A `tool-policy.unknownSupportId` diagnostic for an id the registry lacks,
357
- * attributed to the highest-precedence rule naming it. `level` is absent on a
358
- * feature decision, which has no toolbar level.
360
+ * naming every list that names it. It describes the engine's inputs rather
361
+ * than a decision, so it carries no level.
359
362
  */
360
- export function unknownSupportIdDiagnostic(supportId, origins, level) {
363
+ export function unknownSupportIdDiagnostic(supportId, origins) {
361
364
  const fields = origins.map((rule) => PNP_RULE_FIELDS[rule]).join(", ");
362
365
  return {
363
366
  code: "tool-policy.unknownSupportId",
364
- ...(level ? { level } : {}),
365
367
  toolId: supportId,
366
368
  message: `No tool is registered under "${supportId}", named in ${fields}, so it matches nothing. These lists name tools by tool id.`,
367
369
  details: { origins: [...origins] },