@pie-players/pie-assessment-toolkit 0.3.65 → 0.3.67

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 (48) hide show
  1. package/README.md +36 -16
  2. package/dist/components/ItemToolBar.custom-element.js +1 -1
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +9 -8
  4. package/dist/components/SectionToolBar.custom-element.js +1 -1
  5. package/dist/components/chunks/{ItemToolBar-cckwpz6c.js → ItemToolBar-8jgdz50p.js} +9 -9
  6. package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +2 -1
  9. package/dist/policy/core/PolicySource.d.ts +2 -2
  10. package/dist/policy/core/PolicySource.js +2 -2
  11. package/dist/policy/core/ToolPolicyEngine.d.ts +19 -3
  12. package/dist/policy/core/ToolPolicyEngine.js +39 -5
  13. package/dist/policy/core/compose-decision.d.ts +3 -2
  14. package/dist/policy/core/compose-decision.js +3 -2
  15. package/dist/policy/core/decision-types.d.ts +2 -2
  16. package/dist/policy/core/decision-types.js +2 -2
  17. package/dist/policy/core/feature-decision.d.ts +56 -3
  18. package/dist/policy/core/feature-decision.js +60 -3
  19. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -2
  20. package/dist/policy/core/pnp-policy-inputs.js +2 -2
  21. package/dist/policy/core/provenance.d.ts +4 -1
  22. package/dist/policy/core/provenance.js +4 -1
  23. package/dist/policy/engine.d.ts +1 -1
  24. package/dist/policy/engine.js +1 -0
  25. package/dist/policy/sources/PnpPolicySource.d.ts +2 -1
  26. package/dist/policy/sources/PnpPolicySource.js +2 -1
  27. package/dist/runtime/core/engine-transition.js +3 -1
  28. package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
  29. package/dist/services/AccessibilityCatalogResolver.js +180 -11
  30. package/dist/services/TTSService.d.ts +11 -0
  31. package/dist/services/TTSService.js +220 -32
  32. package/dist/services/ToolRegistry.d.ts +55 -14
  33. package/dist/services/ToolRegistry.js +57 -3
  34. package/dist/services/ToolkitCoordinator.d.ts +18 -1
  35. package/dist/services/ToolkitCoordinator.js +24 -2
  36. package/dist/services/catalog-owner.d.ts +71 -0
  37. package/dist/services/catalog-owner.js +64 -0
  38. package/dist/services/framework-error.d.ts +1 -1
  39. package/dist/services/interfaces.d.ts +15 -3
  40. package/dist/services/tts/browser-provider.js +189 -23
  41. package/dist/tools/content-capability-resolution.d.ts +106 -0
  42. package/dist/tools/content-capability-resolution.js +136 -0
  43. package/dist/tools/internal.d.ts +3 -0
  44. package/dist/tools/internal.js +1 -0
  45. package/package.json +13 -9
  46. package/dist/components/chunks/ItemToolBar-pryf0rtz.js +0 -22
  47. package/dist/runtime/catalog-registration.d.ts +0 -67
  48. package/dist/runtime/catalog-registration.js +0 -86
package/dist/index.d.ts CHANGED
@@ -10,11 +10,11 @@ export type { AssessmentToolkitHostRuntimeContext, AssessmentToolkitRegionScopeC
10
10
  export type { TTSHighlightContext, TTSHighlightTargetResolver, TTSHighlightTargetResolverProvider, TTSHighlightTargetResolverRuntime, } from "./services/tts/highlight-target-resolver.js";
11
11
  export { assessmentToolkitHostRuntimeContext, assessmentToolkitRegionScopeContext, assessmentToolkitRuntimeContext, assessmentToolkitShellContext, } from "./context/assessment-toolkit-context.js";
12
12
  export { connectAssessmentToolkitHostRuntimeContext, connectAssessmentToolkitRegionScopeContext, connectAssessmentToolkitRuntimeContext, connectAssessmentToolkitShellContext, } from "./context/runtime-context-consumer.js";
13
- export { catalogOwnerContextFor, collectCatalogRegistrations, collectEntityCatalogRegistrations, type CatalogOwnerIdentity, type CatalogRegistration, type CatalogSourceEntity, } from "./runtime/catalog-registration.js";
13
+ export { catalogOwnerContextFor, type CatalogOwnerContext, type CatalogOwnerIdentity, type CatalogOwnerKind, type CatalogSourceEntity, } from "./services/catalog-owner.js";
14
14
  export { PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, PIE_REGISTER_EVENT, PIE_UNREGISTER_EVENT, type InternalContentLoadedDetail, type InternalItemSessionChangedDetail, type InternalItemPlayerErrorDetail, type ItemSessionChangedDetail, type RuntimeRegistrationDetail, type RuntimeRegistrationKind, } from "./runtime/registration-events.js";
15
15
  export { connectToolRegionScopeContext, connectToolRuntimeContext, connectToolShellContext, createCrossBoundaryEvent, dispatchCrossBoundaryEvent, isContextValueDefined, } from "./runtime/tool-host-contract.js";
16
16
  export type { AccessibilityCatalogResolverApi, ElementToolStateStoreApi, HighlightCoordinatorApi, I18nServiceApi, ThemeProviderApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi, ToolState, } from "./services/interfaces.js";
17
- export type { CatalogCardForm, CatalogChangeEvent, CatalogChangeListener, CatalogChangeReason, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerContext, CatalogOwnerKind, CatalogStatistics, CatalogType, ResolvedCatalog, } from "./services/AccessibilityCatalogResolver.js";
17
+ export type { CatalogCardForm, CatalogChangeEvent, CatalogChangeListener, CatalogChangeReason, CatalogLookupContext, CatalogLookupOptions, CatalogOwnerCard, CatalogOwnerRegistration, CatalogOwnerSnapshot, CatalogOwnerView, CatalogStatistics, CatalogType, ResolvedCatalog, } from "./services/AccessibilityCatalogResolver.js";
18
18
  export { AccessibilityCatalogResolver, catalogCardForm, isKnownCatalogType, KNOWN_CATALOG_TYPES, } from "./services/AccessibilityCatalogResolver.js";
19
19
  export { ContextVariableStore } from "./services/ContextVariableStore.js";
20
20
  export { ElementToolStateStore } from "./services/ElementToolStateStore.js";
@@ -24,7 +24,7 @@ export type { SerializedRange } from "./services/RangeSerializer.js";
24
24
  export { RangeSerializer } from "./services/RangeSerializer.js";
25
25
  export type { I18nConfig, PluralTranslation, TranslationBundle, } from "./services/I18nService.js";
26
26
  export { I18nService } from "./services/I18nService.js";
27
- export type { ResolvedToolContext, ToolbarContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap, ToolContextResolverResult, ToolActivation, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolWindowShellAction, ToolWindowShellAlign, ToolWindowShellConfig, ToolWindowShellContentConfig, ToolRegistration, } from "./services/ToolRegistry.js";
27
+ export type { ResolvedToolContext, ToolbarContext, ToolContextResolver, ToolContextResolverContext, ToolContextResolverMap, ToolContextResolverResult, ToolActivation, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolRegistryChangeEvent, ToolRegistryChangeKind, ToolRegistryChangeListener, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolWindowShellAction, ToolWindowShellAlign, ToolWindowShellConfig, ToolWindowShellContentConfig, ToolRegistration, } from "./services/ToolRegistry.js";
28
28
  export { ToolRegistry } from "./services/ToolRegistry.js";
29
29
  export type { AssessmentToolContext, BaseToolContext, ElementToolContext, ItemToolContext, PassageToolContext, RubricToolContext, SectionToolContext, ToolContext, ToolLevel, } from "./services/tool-context.js";
30
30
  export { extractTextContent, hasChoiceInteraction, hasMathContent, hasReadableText, isAssessmentContext, isElementContext, isItemContext, isPassageContext, isRubricContext, isSectionContext, } from "./services/tool-context.js";
@@ -53,6 +53,7 @@ export type { ToolConfigDiagnostic, ToolConfigDiagnosticSeverity, ToolConfigStri
53
53
  export type { ToolbarButtonItem, ToolbarItem, ToolbarItemBase, ToolbarLinkItem, } from "./services/toolbar-items.js";
54
54
  export { isExternalIconUrl, isInlineSvgIcon, isToolbarLinkItem, isValidToolbarItemShape, } from "./services/toolbar-items.js";
55
55
  export { normalizeToolsConfig, normalizeToolAlias, normalizeToolList, parseToolList, } from "./services/tools-config-normalizer.js";
56
+ export { isHostDeniedFeature } from "./policy/core/feature-decision.js";
56
57
  export { frameworkErrorFromToolConfigValidation, normalizeAndValidateToolsConfig, } from "./services/tool-config-validation.js";
57
58
  export type { ParsedToolInstanceId, ToolScopeLevel, } from "./services/tool-instance-id.js";
58
59
  export { createScopedToolId, parseScopedToolId, toOverlayToolId, } from "./services/tool-instance-id.js";
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export { assessmentToolkitHostRuntimeContext, assessmentToolkitRegionScopeContext, assessmentToolkitRuntimeContext, assessmentToolkitShellContext, } from "./context/assessment-toolkit-context.js";
10
10
  export { connectAssessmentToolkitHostRuntimeContext, connectAssessmentToolkitRegionScopeContext, connectAssessmentToolkitRuntimeContext, connectAssessmentToolkitShellContext, } from "./context/runtime-context-consumer.js";
11
- export { catalogOwnerContextFor, collectCatalogRegistrations, collectEntityCatalogRegistrations, } from "./runtime/catalog-registration.js";
11
+ export { catalogOwnerContextFor, } from "./services/catalog-owner.js";
12
12
  export { PIE_INTERNAL_CONTENT_LOADED_EVENT, PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT, PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT, PIE_ITEM_SESSION_CHANGED_EVENT, PIE_REGISTER_EVENT, PIE_UNREGISTER_EVENT, } from "./runtime/registration-events.js";
13
13
  export { connectToolRegionScopeContext, connectToolRuntimeContext, connectToolShellContext, createCrossBoundaryEvent, dispatchCrossBoundaryEvent, isContextValueDefined, } from "./runtime/tool-host-contract.js";
14
14
  export { AccessibilityCatalogResolver, catalogCardForm, isKnownCatalogType, KNOWN_CATALOG_TYPES, } from "./services/AccessibilityCatalogResolver.js";
@@ -42,6 +42,7 @@ export { ToolkitCoordinator } from "./services/ToolkitCoordinator.js";
42
42
  export { formatFrameworkErrorForConsole, frameworkErrorFromToolConfigDiagnostics, frameworkErrorFromUnknown, toFrameworkErrorModel, } from "./services/framework-error.js";
43
43
  export { isExternalIconUrl, isInlineSvgIcon, isToolbarLinkItem, isValidToolbarItemShape, } from "./services/toolbar-items.js";
44
44
  export { normalizeToolsConfig, normalizeToolAlias, normalizeToolList, parseToolList, } from "./services/tools-config-normalizer.js";
45
+ export { isHostDeniedFeature } from "./policy/core/feature-decision.js";
45
46
  export { frameworkErrorFromToolConfigValidation, normalizeAndValidateToolsConfig, } from "./services/tool-config-validation.js";
46
47
  export { createScopedToolId, parseScopedToolId, toOverlayToolId, } from "./services/tool-instance-id.js";
47
48
  export { PlaybackState, TTSService } from "./services/TTSService.js";
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Custom `PolicySource` extension point (M8 — see
3
- * `.cursor/plans/m8-design.md` § 5).
2
+ * Custom `PolicySource` extension point. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Custom sources run in step 6 of the composition pipeline — *after*
6
6
  * placement membership, provider veto, host policy, and PNP/profile gates.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Custom `PolicySource` extension point (M8 — see
3
- * `.cursor/plans/m8-design.md` § 5).
2
+ * Custom `PolicySource` extension point. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Custom sources run in step 6 of the composition pipeline — *after*
6
6
  * placement membership, provider veto, host policy, and PNP/profile gates.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tool Policy Engine — facade class (M8 — see
3
- * `.cursor/plans/m8-design.md` § 2).
2
+ * Tool Policy Engine — facade class. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Wraps `composeDecision(...)` with engine-instance state:
6
6
  * - bound inputs (`tools`, `assessment`, `currentItemRef`, ...)
@@ -33,7 +33,7 @@ export interface ToolPolicyEngineInputs {
33
33
  * `assessment` and `currentItemRef`: `"on"` if they carry any
34
34
  * PNP/profile policy material (PNP, district policy, test
35
35
  * administration, item-level required/restricted/parameters),
36
- * `"off"` otherwise. See `.cursor/plans/m8-design.md` F2.
36
+ * `"off"` otherwise. `tests/policy/pnp-default-on.test.ts` locks the rule.
37
37
  *
38
38
  * Hosts that want to force a mode pass `"on"` or `"off"`
39
39
  * explicitly. Embedded under `<pie-section-player-*>` the preferred
@@ -107,8 +107,24 @@ export declare class ToolPolicyEngine {
107
107
  * whenever profile material exists, so this only diverges for a host that
108
108
  * explicitly forces `"off"` while supplying a profile that grants the
109
109
  * feature — and there, honouring the profile is the safer failure.
110
+ *
111
+ * The decision reports whether an assessment was bound, which is the engine's
112
+ * to answer rather than the policy source's — see
113
+ * {@link FeaturePolicyDecision.assessmentBound}.
110
114
  */
111
115
  decideFeature(featureId: string): FeaturePolicyDecision;
116
+ /**
117
+ * The host gates that hold for a feature id, or `null` when none fires.
118
+ *
119
+ * `policy.allowed` / `policy.blocked` name capabilities, not placements, so
120
+ * they are the one part of the host pipeline that is meaningful without a
121
+ * placement level — and the only lever a host has over a capability that
122
+ * renders as its own surface, since a `region` capability is rejected from
123
+ * `tools.placement` by configuration validation. `provider-disabled` and
124
+ * `placement-membership` are deliberately not applied: both are statements
125
+ * about a toolbar the feature was never on.
126
+ */
127
+ private hostFeatureGate;
112
128
  /**
113
129
  * Convenience wrapper for hosts that just want the visible tool
114
130
  * IDs. Equivalent to `decide(...).visibleTools.map(e => e.toolId)`.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tool Policy Engine — facade class (M8 — see
3
- * `.cursor/plans/m8-design.md` § 2).
2
+ * Tool Policy Engine — facade class. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Wraps `composeDecision(...)` with engine-instance state:
6
6
  * - bound inputs (`tools`, `assessment`, `currentItemRef`, ...)
@@ -16,8 +16,8 @@
16
16
  * PR 1 ships the engine *without callers*. PR 2 wires it into
17
17
  * `ToolkitCoordinator`. PR 3 switches `<pie-item-toolbar>` over.
18
18
  */
19
- import { normalizeToolsConfig } from "../../services/tools-config-normalizer.js";
20
- import { interpretFeatureResult } from "./feature-decision.js";
19
+ import { normalizeToolList, normalizeToolsConfig, } from "../../services/tools-config-normalizer.js";
20
+ import { hostFeatureDenial, interpretFeatureResult, } from "./feature-decision.js";
21
21
  import { composeDecision } from "./compose-decision.js";
22
22
  import { resolveDefaultPnpEnforcement } from "./pnp-policy-inputs.js";
23
23
  import { PnpPolicySource } from "../sources/PnpPolicySource.js";
@@ -104,13 +104,47 @@ export class ToolPolicyEngine {
104
104
  * whenever profile material exists, so this only diverges for a host that
105
105
  * explicitly forces `"off"` while supplying a profile that grants the
106
106
  * feature — and there, honouring the profile is the safer failure.
107
+ *
108
+ * The decision reports whether an assessment was bound, which is the engine's
109
+ * to answer rather than the policy source's — see
110
+ * {@link FeaturePolicyDecision.assessmentBound}.
107
111
  */
108
112
  decideFeature(featureId) {
109
113
  this.assertNotDisposed();
114
+ const hostDenial = this.hostFeatureGate(featureId);
115
+ if (hostDenial)
116
+ return hostDenial;
110
117
  return interpretFeatureResult(featureId, this.pnpPolicySource.resolveFeature(featureId, {
111
118
  assessment: this.assessment ?? undefined,
112
119
  currentItemRef: this.currentItemRef ?? undefined,
113
- }));
120
+ }),
121
+ // `resolveFeature` takes the assessment as `undefined` either way, so the
122
+ // source cannot tell an unbound host from one whose profile is silent.
123
+ // The engine can.
124
+ { assessmentBound: this.assessment !== null });
125
+ }
126
+ /**
127
+ * The host gates that hold for a feature id, or `null` when none fires.
128
+ *
129
+ * `policy.allowed` / `policy.blocked` name capabilities, not placements, so
130
+ * they are the one part of the host pipeline that is meaningful without a
131
+ * placement level — and the only lever a host has over a capability that
132
+ * renders as its own surface, since a `region` capability is rejected from
133
+ * `tools.placement` by configuration validation. `provider-disabled` and
134
+ * `placement-membership` are deliberately not applied: both are statements
135
+ * about a toolbar the feature was never on.
136
+ */
137
+ hostFeatureGate(featureId) {
138
+ const context = { assessmentBound: this.assessment !== null };
139
+ const blocked = normalizeToolList(this.tools.policy.blocked);
140
+ if (blocked.includes(featureId)) {
141
+ return hostFeatureDenial(featureId, "host-blocked", blocked, context);
142
+ }
143
+ const allowed = normalizeToolList(this.tools.policy.allowed);
144
+ if (allowed.length > 0 && !allowed.includes(featureId)) {
145
+ return hostFeatureDenial(featureId, "host-allowlist", allowed, context);
146
+ }
147
+ return null;
114
148
  }
115
149
  /**
116
150
  * Convenience wrapper for hosts that just want the visible tool
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Compose-decision pipeline (M8 — see `.cursor/plans/m8-design.md` § 3).
2
+ * Compose-decision pipeline. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
3
4
  *
4
5
  * Pure function: given a decision request, the host's tools config,
5
6
  * an optional PNP policy source, and the registered custom sources, returns
@@ -7,7 +8,7 @@
7
8
  * mutable state, and never reaches into Svelte / DOM — that is the
8
9
  * `ToolPolicyEngine` class's job.
9
10
  *
10
- * The six steps below mirror the design doc exactly. The tests in
11
+ * The tests in
11
12
  * `tests/policy/compose-decision.test.ts` lock the orchestration in.
12
13
  */
13
14
  import type { AssessmentEntity, AssessmentItemRef } from "@pie-players/pie-players-shared/types";
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Compose-decision pipeline (M8 — see `.cursor/plans/m8-design.md` § 3).
2
+ * Compose-decision pipeline. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
3
4
  *
4
5
  * Pure function: given a decision request, the host's tools config,
5
6
  * an optional PNP policy source, and the registered custom sources, returns
@@ -7,7 +8,7 @@
7
8
  * mutable state, and never reaches into Svelte / DOM — that is the
8
9
  * `ToolPolicyEngine` class's job.
9
10
  *
10
- * The six steps below mirror the design doc exactly. The tests in
11
+ * The tests in
11
12
  * `tests/policy/compose-decision.test.ts` lock the orchestration in.
12
13
  */
13
14
  import { normalizeToolList, } from "../../services/tools-config-normalizer.js";
@@ -6,8 +6,8 @@
6
6
  * composition pipeline that produces the response, lives in sibling
7
7
  * modules.
8
8
  *
9
- * See `.cursor/plans/m8-design.md` § 2 for the engine shape, § 3 for
10
- * the composition rule, and § 4 for the provenance contract.
9
+ * `docs/tools-and-accomodations/architecture.md` covers the engine shape,
10
+ * the composition rule, and the provenance contract.
11
11
  */
12
12
  import type { ToolPlacementLevel } from "../../services/tools-config-normalizer.js";
13
13
  import type { ToolContext, ToolLevel } from "../../services/tool-context.js";
@@ -6,7 +6,7 @@
6
6
  * composition pipeline that produces the response, lives in sibling
7
7
  * modules.
8
8
  *
9
- * See `.cursor/plans/m8-design.md` § 2 for the engine shape, § 3 for
10
- * the composition rule, and § 4 for the provenance contract.
9
+ * `docs/tools-and-accomodations/architecture.md` covers the engine shape,
10
+ * the composition rule, and the provenance contract.
11
11
  */
12
12
  export {};
@@ -15,10 +15,21 @@
15
15
  * feature id?* Whether the capability has anything to show is a separate,
16
16
  * independent check owned by its renderer — for signing, whether a matching
17
17
  * catalog card exists. Both are required; neither implies the other.
18
+ *
19
+ * A denial carries `assessmentBound`, because "nobody asked for this" and
20
+ * "nothing was bound to ask against" are the same verdict for different
21
+ * reasons, and only the second is a defect.
18
22
  */
19
23
  import type { PnpPolicyResult } from "../sources/PnpPolicySource.js";
20
24
  import type { PnpPolicySourceRule } from "./policy-source-tag.js";
21
25
  import type { ToolPolicyResolutionDecision, ToolPolicySourceType } from "./provenance.js";
26
+ /**
27
+ * A feature verdict comes from one of the six PNP precedence levels, or from a
28
+ * host gate that never reaches them: `tools.policy.blocked` and a non-empty
29
+ * `tools.policy.allowed` are absolute for the id they name, exactly as they are
30
+ * on the placement-scoped path.
31
+ */
32
+ export type FeaturePolicyRule = PnpPolicySourceRule | "host-allowlist" | "host-blocked";
22
33
  export interface FeaturePolicyDecision {
23
34
  /** The PNP/AfA support id that was evaluated (e.g. `"signLanguage"`). */
24
35
  featureId: string;
@@ -30,11 +41,29 @@ export interface FeaturePolicyDecision {
30
41
  granted: boolean;
31
42
  action: ToolPolicyResolutionDecision["action"];
32
43
  /** Which precedence rule produced the verdict. */
33
- rule: PnpPolicySourceRule;
34
- precedence: 1 | 2 | 3 | 4 | 5 | 6;
44
+ rule: FeaturePolicyRule;
45
+ precedence: 0 | 1 | 2 | 3 | 4 | 5 | 6;
35
46
  sourceType: ToolPolicySourceType;
36
47
  /** Human-readable explanation, suitable for a policy debugger. */
37
48
  reason: string;
49
+ /**
50
+ * Whether an assessment was bound when this was decided.
51
+ *
52
+ * `false` makes a *denial* a wiring gap rather than a verdict: no profile,
53
+ * 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.
57
+ *
58
+ * Granting is unaffected either way: an unbound host with no item mandate still
59
+ * gets `granted: false`, since an accommodation requires a documented need and
60
+ * an absent profile documents nothing.
61
+ *
62
+ * A bound assessment carrying no profile material is deliberately `true`: a
63
+ * test that grants nobody an accommodation is a legitimate configuration,
64
+ * while never binding one cannot be.
65
+ */
66
+ assessmentBound: boolean;
38
67
  /**
39
68
  * `true` when the grant is a mandate (item or district `requiredTools`)
40
69
  * rather than a student-profile support.
@@ -47,6 +76,30 @@ export interface FeaturePolicyDecision {
47
76
  */
48
77
  parameters?: unknown;
49
78
  }
79
+ /** What the engine knows that a single support-id resolution does not. */
80
+ export interface FeatureDecisionContext {
81
+ /** Whether the engine has an assessment bound. */
82
+ assessmentBound: boolean;
83
+ }
84
+ /**
85
+ * A host gate denied the feature outright, so no policy source was consulted.
86
+ *
87
+ * `precedence: 0` and the two `host-*` rules are the same values
88
+ * `composeDecision(...)` records for the placement-scoped path, so a debugger
89
+ * reads one vocabulary for both. `assessmentBound` is still reported: a host
90
+ * blocklist is a verdict whether or not an assessment was bound, and the flag
91
+ * only ever qualifies a *policy* denial.
92
+ */
93
+ export declare function hostFeatureDenial(featureId: string, rule: "host-allowlist" | "host-blocked", hostValue: readonly string[], context: FeatureDecisionContext): FeaturePolicyDecision;
94
+ /**
95
+ * Whether a host gate produced the verdict, rather than a policy source.
96
+ *
97
+ * The distinction is load-bearing for a content-dependent capability that
98
+ * declares `resolvesWithoutGrant`: an absent grant is a case it is allowed to
99
+ * answer from the content alone, while a host denial is the host's off switch
100
+ * and nothing may reopen it.
101
+ */
102
+ export declare function isHostDeniedFeature(decision: Pick<FeaturePolicyDecision, "rule"> | null | undefined): boolean;
50
103
  /**
51
104
  * Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
52
105
  *
@@ -54,4 +107,4 @@ export interface FeaturePolicyDecision {
54
107
  * exactly one decision and at most one flags entry — no mapped-tool-id
55
108
  * bookkeeping is needed to read it back out.
56
109
  */
57
- export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult): FeaturePolicyDecision;
110
+ export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult, context: FeatureDecisionContext): FeaturePolicyDecision;
@@ -15,7 +15,55 @@
15
15
  * feature id?* Whether the capability has anything to show is a separate,
16
16
  * independent check owned by its renderer — for signing, whether a matching
17
17
  * catalog card exists. Both are required; neither implies the other.
18
+ *
19
+ * A denial carries `assessmentBound`, because "nobody asked for this" and
20
+ * "nothing was bound to ask against" are the same verdict for different
21
+ * reasons, and only the second is a defect.
22
+ */
23
+ /**
24
+ * Reason text for a denial that had no assessment to decide against.
25
+ *
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.
18
30
  */
31
+ const unboundAssessmentReason = (featureId) => `No assessment is bound, so no policy source could grant "${featureId}"`;
32
+ /**
33
+ * A host gate denied the feature outright, so no policy source was consulted.
34
+ *
35
+ * `precedence: 0` and the two `host-*` rules are the same values
36
+ * `composeDecision(...)` records for the placement-scoped path, so a debugger
37
+ * reads one vocabulary for both. `assessmentBound` is still reported: a host
38
+ * blocklist is a verdict whether or not an assessment was bound, and the flag
39
+ * only ever qualifies a *policy* denial.
40
+ */
41
+ export function hostFeatureDenial(featureId, rule, hostValue, context) {
42
+ return {
43
+ featureId,
44
+ granted: false,
45
+ action: "block",
46
+ rule,
47
+ precedence: 0,
48
+ sourceType: "host",
49
+ reason: rule === "host-blocked"
50
+ ? "Listed in tools.policy.blocked"
51
+ : `Not listed in tools.policy.allowed (${hostValue.join(", ")})`,
52
+ required: false,
53
+ assessmentBound: context.assessmentBound,
54
+ };
55
+ }
56
+ /**
57
+ * Whether a host gate produced the verdict, rather than a policy source.
58
+ *
59
+ * The distinction is load-bearing for a content-dependent capability that
60
+ * declares `resolvesWithoutGrant`: an absent grant is a case it is allowed to
61
+ * answer from the content alone, while a host denial is the host's off switch
62
+ * and nothing may reopen it.
63
+ */
64
+ export function isHostDeniedFeature(decision) {
65
+ return (decision?.rule === "host-blocked" || decision?.rule === "host-allowlist");
66
+ }
19
67
  /**
20
68
  * Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
21
69
  *
@@ -23,18 +71,27 @@
23
71
  * exactly one decision and at most one flags entry — no mapped-tool-id
24
72
  * bookkeeping is needed to read it back out.
25
73
  */
26
- export function interpretFeatureResult(featureId, result) {
74
+ export function interpretFeatureResult(featureId, result, context) {
27
75
  const decision = result.decisions[0];
28
76
  const flags = Array.from(result.perToolFlags.values())[0];
77
+ const granted = decision?.action === "enable";
78
+ const reason = decision?.reason ?? `Feature "${featureId}" not configured`;
29
79
  return {
30
80
  featureId,
31
- granted: decision?.action === "enable",
81
+ granted,
32
82
  action: decision?.action ?? "skip",
33
83
  rule: decision?.rule ?? "pnp-support",
34
84
  precedence: decision?.precedence ?? 6,
35
85
  sourceType: decision?.sourceType ?? "system",
36
- reason: decision?.reason ?? `Feature "${featureId}" not configured`,
86
+ // 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
+ reason: context.assessmentBound || granted
91
+ ? reason
92
+ : unboundAssessmentReason(featureId),
37
93
  required: Boolean(flags?.required),
38
94
  parameters: flags?.settings,
95
+ assessmentBound: context.assessmentBound,
39
96
  };
40
97
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * PNP/profile policy input detection (M8 PR 4 — see `.cursor/plans/m8-design.md` F2 and
3
- * `.cursor/plans/m8-implementation-plan.md` § PR 4).
2
+ * PNP/profile policy input detection. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Pure helpers that decide whether the inputs the engine has been given
6
6
  * actually carry PNP/profile policy material. Used by:
@@ -1,6 +1,6 @@
1
1
  /**
2
- * PNP/profile policy input detection (M8 PR 4 — see `.cursor/plans/m8-design.md` F2 and
3
- * `.cursor/plans/m8-implementation-plan.md` § PR 4).
2
+ * PNP/profile policy input detection. See
3
+ * `docs/tools-and-accomodations/architecture.md`.
4
4
  *
5
5
  * Pure helpers that decide whether the inputs the engine has been given
6
6
  * actually carry PNP/profile policy material. Used by:
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Tool Policy Provenance (M8 — see `.cursor/plans/m8-design.md` § 4).
2
+ * Tool Policy Provenance.
3
+ *
4
+ * Precedence and the rule vocabulary are documented in
5
+ * `docs/tools-and-accomodations/architecture.md`.
3
6
  *
4
7
  * Tracks decisions from every Pass-1 contributor: host placement, host policy,
5
8
  * provider veto, PNP/profile gates, and custom policy sources.
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Tool Policy Provenance (M8 — see `.cursor/plans/m8-design.md` § 4).
2
+ * Tool Policy Provenance.
3
+ *
4
+ * Precedence and the rule vocabulary are documented in
5
+ * `docs/tools-and-accomodations/architecture.md`.
3
6
  *
4
7
  * Tracks decisions from every Pass-1 contributor: host placement, host policy,
5
8
  * provider veto, PNP/profile gates, and custom policy sources.
@@ -21,7 +21,7 @@
21
21
  */
22
22
  export { ToolPolicyEngine, type PnpEnforcementMode, type ResolvedEngineInputs, type ToolPolicyChangeEvent, type ToolPolicyChangeListener, type ToolPolicyEngineArgs, type ToolPolicyEngineInputs, } from "./core/ToolPolicyEngine.js";
23
23
  export { TOOL_POLICY_ENGINE_KEY, type ToolPolicyEngineContext, } from "./core/engine-context.js";
24
- export type { FeaturePolicyDecision } from "./core/feature-decision.js";
24
+ export { isHostDeniedFeature, type FeaturePolicyDecision, type FeaturePolicyRule, } from "./core/feature-decision.js";
25
25
  export type { RequiredToolBlockedDetails, ToolPolicyDecision, ToolPolicyDecisionRequest, ToolPolicyDiagnostic, ToolPolicyDiagnosticCode, ToolPolicyEntry, ToolPolicyHostGate, ToolScope, } from "./core/decision-types.js";
26
26
  export type { PolicySource, PolicySourceDecisionContext, PolicySourceProvenanceEntry, PolicySourceResult, } from "./core/PolicySource.js";
27
27
  export type { PolicySourceTag, PnpPolicySourceRule, PnpPolicySourceTag, CustomPolicySourceTag, } from "./core/policy-source-tag.js";
@@ -21,3 +21,4 @@
21
21
  */
22
22
  export { ToolPolicyEngine, } from "./core/ToolPolicyEngine.js";
23
23
  export { TOOL_POLICY_ENGINE_KEY, } from "./core/engine-context.js";
24
+ export { isHostDeniedFeature, } from "./core/feature-decision.js";
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * PNP Policy Source for the M8 engine
3
- * (see `.cursor/plans/m8-design.md` § 3 step 5 and § 5).
3
+ * (see
4
+ * `docs/tools-and-accomodations/architecture.md`).
4
5
  *
5
6
  * This source applies the PNP/profile precedence rules as a `(candidates, pnpPolicyInputs) →
6
7
  * (refinedCandidates, perToolFlags, mandates, decisions)` function the
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * PNP Policy Source for the M8 engine
3
- * (see `.cursor/plans/m8-design.md` § 3 step 5 and § 5).
3
+ * (see
4
+ * `docs/tools-and-accomodations/architecture.md`).
4
5
  *
5
6
  * This source applies the PNP/profile precedence rules as a `(candidates, pnpPolicyInputs) →
6
7
  * (refinedCandidates, perToolFlags, mandates, decisions)` function the
@@ -227,7 +227,9 @@ export function transition(state, input) {
227
227
  lastFrameworkError: input.error,
228
228
  readinessSignals: {
229
229
  ...state.readinessSignals,
230
- runtimeError: true,
230
+ runtimeError: input.error.recoverable === true
231
+ ? state.readinessSignals.runtimeError
232
+ : true,
231
233
  },
232
234
  },
233
235
  outputs: [{ kind: "framework-error", error: input.error }],
@@ -1,15 +1,38 @@
1
1
  import type { AccessibilityCatalog, CatalogCard, CatalogCardPayload } from "@pie-players/pie-players-shared/types";
2
- export type CatalogOwnerKind = "global" | "passage" | "itemModel";
3
- export interface CatalogOwnerContext {
4
- ownerKind: CatalogOwnerKind;
5
- assessmentId?: string;
6
- sectionId?: string;
7
- canonicalItemId?: string;
8
- itemId?: string;
9
- passageId?: string;
10
- modelId?: string;
11
- }
2
+ import { type CatalogOwnerContext, type CatalogOwnerIdentity, type CatalogSourceEntity } from "./catalog-owner.js";
12
3
  export type CatalogLookupContext = CatalogOwnerContext;
4
+ /** One card visible from a mounted content owner's catalog scope. */
5
+ export interface CatalogOwnerCard {
6
+ /** Author-owned identifier; never normalized or prefixed. */
7
+ readonly catalogId: string;
8
+ /** Read-only authored card, including capability-specific metadata. */
9
+ readonly card: Readonly<CatalogCard>;
10
+ }
11
+ /**
12
+ * Immutable point-in-time view of the cards owned by an item or passage.
13
+ *
14
+ * Ordering is deterministic and matches registration precedence: entity-root,
15
+ * extractor-generated, then model-owned catalogs in model order. Capabilities
16
+ * interpret card meaning; the generic resolver owns only scope and traversal.
17
+ */
18
+ export interface CatalogOwnerSnapshot {
19
+ readonly cards: readonly CatalogOwnerCard[];
20
+ }
21
+ /**
22
+ * Owner-bound catalog interface used by content capabilities and their host.
23
+ *
24
+ * The owner context is captured once. Callers no longer coordinate a raw entity,
25
+ * a resolver and a separately assembled lookup context.
26
+ */
27
+ export interface CatalogOwnerView {
28
+ snapshot(): CatalogOwnerSnapshot;
29
+ onChange(listener: () => void): () => void;
30
+ }
31
+ /** Everything needed to register one mounted catalog owner. */
32
+ export interface CatalogOwnerRegistration {
33
+ owner: CatalogOwnerIdentity;
34
+ entity: CatalogSourceEntity | null | undefined;
35
+ }
13
36
  /** What changed in the resolver's catalog set. */
14
37
  export type CatalogChangeReason = "scoped-registered" | "scoped-removed" | "item-added" | "item-cleared";
15
38
  /**
@@ -155,6 +178,7 @@ export declare class AccessibilityCatalogResolver {
155
178
  private defaultLanguage;
156
179
  private sanitizedSpokenCache;
157
180
  private catalogChangeListeners;
181
+ private reportedSnapshotProblems;
158
182
  constructor(assessmentCatalogs?: AccessibilityCatalog[], defaultLanguage?: string);
159
183
  /**
160
184
  * Set the default language for fallback resolution
@@ -191,7 +215,19 @@ export declare class AccessibilityCatalogResolver {
191
215
  * Index catalogs into the appropriate map
192
216
  */
193
217
  private indexCatalogs;
218
+ private insertScopedCatalogs;
219
+ private removeScopedInsertion;
194
220
  registerCatalogs(context: CatalogOwnerContext, catalogs?: AccessibilityCatalog[]): () => void;
221
+ /**
222
+ * Register every catalog carried by one mounted item or passage as one
223
+ * owner-level transaction.
224
+ *
225
+ * The resolver owns the entity walk and emits one post-mutation signal, so a
226
+ * reader never observes only the root or only the model half of an owner.
227
+ */
228
+ registerOwner(registration: CatalogOwnerRegistration): () => void;
229
+ /** Bind catalog reads and change observation to one content owner. */
230
+ forOwner(context: CatalogOwnerContext): CatalogOwnerView;
195
231
  /**
196
232
  * Add item-level catalogs (called when rendering a new item)
197
233
  */
@@ -216,6 +252,9 @@ export declare class AccessibilityCatalogResolver {
216
252
  private scopedCatalogEntries;
217
253
  private findScopedCandidates;
218
254
  private isCompatibleOwnerContext;
255
+ private ownerScopedCatalogEntries;
256
+ private createOwnerSnapshot;
257
+ private catalogChangeAffectsOwner;
219
258
  private sanitizeCatalogs;
220
259
  /**
221
260
  * Find a matching catalog card based on lookup options