@pie-players/pie-assessment-toolkit 0.3.64 → 0.3.66

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 (74) hide show
  1. package/README.md +81 -33
  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-8jgdz50p.js +51 -0
  6. package/dist/components/chunks/ItemToolBar-cvs646j3.js +36 -0
  7. package/dist/index.d.ts +7 -8
  8. package/dist/index.js +10 -5
  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 +7 -3
  12. package/dist/policy/core/ToolPolicyEngine.js +11 -3
  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 +28 -1
  18. package/dist/policy/core/feature-decision.js +25 -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/sources/PnpPolicySource.d.ts +2 -1
  24. package/dist/policy/sources/PnpPolicySource.js +2 -1
  25. package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
  26. package/dist/runtime/composition-emit-scheduler.js +154 -0
  27. package/dist/runtime/core/engine-resolver.d.ts +1 -1
  28. package/dist/runtime/core/engine-transition.js +3 -1
  29. package/dist/services/AccessibilityCatalogResolver.d.ts +49 -10
  30. package/dist/services/AccessibilityCatalogResolver.js +180 -11
  31. package/dist/services/TTSService.d.ts +11 -0
  32. package/dist/services/TTSService.js +220 -32
  33. package/dist/services/ToolRegistry.d.ts +259 -8
  34. package/dist/services/ToolRegistry.js +181 -11
  35. package/dist/services/ToolkitCoordinator.d.ts +20 -2
  36. package/dist/services/ToolkitCoordinator.js +47 -8
  37. package/dist/services/catalog-owner.d.ts +71 -0
  38. package/dist/services/catalog-owner.js +64 -0
  39. package/dist/services/createDefaultToolRegistry.d.ts +25 -58
  40. package/dist/services/createDefaultToolRegistry.js +24 -104
  41. package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -14
  42. package/dist/services/defaultPersonalNeedsProfile.js +17 -38
  43. package/dist/services/framework-error.d.ts +1 -1
  44. package/dist/services/interfaces.d.ts +15 -3
  45. package/dist/services/pnp-standard-features.d.ts +1 -1
  46. package/dist/services/tool-config-defaults.d.ts +7 -23
  47. package/dist/services/tool-config-defaults.js +7 -46
  48. package/dist/services/tool-config-validation.d.ts +1 -1
  49. package/dist/services/tool-config-validation.js +44 -4
  50. package/dist/services/tts/browser-provider.js +191 -24
  51. package/dist/services/tts-runtime-config.js +7 -2
  52. package/dist/tools/internal.d.ts +35 -0
  53. package/dist/tools/internal.js +33 -0
  54. package/dist/tools/tool-tag-map.d.ts +15 -3
  55. package/dist/tools/tool-tag-map.js +21 -18
  56. package/package.json +14 -10
  57. package/dist/components/chunks/ItemToolBar-3cppre9r.js +0 -51
  58. package/dist/components/chunks/ItemToolBar-7rq2gj8b.js +0 -22
  59. package/dist/runtime/catalog-registration.d.ts +0 -67
  60. package/dist/runtime/catalog-registration.js +0 -86
  61. package/dist/services/sign-language-cards.d.ts +0 -82
  62. package/dist/services/sign-language-cards.js +0 -133
  63. package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
  64. package/dist/tools/registrations/accessibility-tools.js +0 -217
  65. package/dist/tools/registrations/calculator.d.ts +0 -20
  66. package/dist/tools/registrations/calculator.js +0 -228
  67. package/dist/tools/registrations/interaction-tools.d.ts +0 -27
  68. package/dist/tools/registrations/interaction-tools.js +0 -143
  69. package/dist/tools/registrations/measurement-tools.d.ts +0 -24
  70. package/dist/tools/registrations/measurement-tools.js +0 -130
  71. package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
  72. package/dist/tools/registrations/subject-specific-tools.js +0 -158
  73. package/dist/tools/registrations/tts.d.ts +0 -21
  74. package/dist/tools/registrations/tts.js +0 -184
@@ -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,6 +15,10 @@
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";
@@ -35,6 +39,24 @@ export interface FeaturePolicyDecision {
35
39
  sourceType: ToolPolicySourceType;
36
40
  /** Human-readable explanation, suitable for a policy debugger. */
37
41
  reason: string;
42
+ /**
43
+ * Whether an assessment was bound when this was decided.
44
+ *
45
+ * `false` makes a *denial* a wiring gap rather than a verdict: no profile,
46
+ * district policy or test administration could be consulted, because there was
47
+ * no assessment to read them from. It is not itself a denial — an item ref
48
+ * carrying `requiredTools` mandates a feature at precedence 4 with no
49
+ * assessment bound — so read it alongside `granted` rather than instead of it.
50
+ *
51
+ * Granting is unaffected either way: an unbound host with no item mandate still
52
+ * gets `granted: false`, since an accommodation requires a documented need and
53
+ * an absent profile documents nothing.
54
+ *
55
+ * A bound assessment carrying no profile material is deliberately `true`: a
56
+ * test that grants nobody an accommodation is a legitimate configuration,
57
+ * while never binding one cannot be.
58
+ */
59
+ assessmentBound: boolean;
38
60
  /**
39
61
  * `true` when the grant is a mandate (item or district `requiredTools`)
40
62
  * rather than a student-profile support.
@@ -47,6 +69,11 @@ export interface FeaturePolicyDecision {
47
69
  */
48
70
  parameters?: unknown;
49
71
  }
72
+ /** What the engine knows that a single support-id resolution does not. */
73
+ export interface FeatureDecisionContext {
74
+ /** Whether the engine has an assessment bound. */
75
+ assessmentBound: boolean;
76
+ }
50
77
  /**
51
78
  * Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
52
79
  *
@@ -54,4 +81,4 @@ export interface FeaturePolicyDecision {
54
81
  * exactly one decision and at most one flags entry — no mapped-tool-id
55
82
  * bookkeeping is needed to read it back out.
56
83
  */
57
- export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult): FeaturePolicyDecision;
84
+ export declare function interpretFeatureResult(featureId: string, result: PnpPolicyResult, context: FeatureDecisionContext): FeaturePolicyDecision;
@@ -15,7 +15,20 @@
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}"`;
19
32
  /**
20
33
  * Interpret a single-feature `PnpPolicySource.resolveFeature(...)` result.
21
34
  *
@@ -23,18 +36,27 @@
23
36
  * exactly one decision and at most one flags entry — no mapped-tool-id
24
37
  * bookkeeping is needed to read it back out.
25
38
  */
26
- export function interpretFeatureResult(featureId, result) {
39
+ export function interpretFeatureResult(featureId, result, context) {
27
40
  const decision = result.decisions[0];
28
41
  const flags = Array.from(result.perToolFlags.values())[0];
42
+ const granted = decision?.action === "enable";
43
+ const reason = decision?.reason ?? `Feature "${featureId}" not configured`;
29
44
  return {
30
45
  featureId,
31
- granted: decision?.action === "enable",
46
+ granted,
32
47
  action: decision?.action ?? "skip",
33
48
  rule: decision?.rule ?? "pnp-support",
34
49
  precedence: decision?.precedence ?? 6,
35
50
  sourceType: decision?.sourceType ?? "system",
36
- reason: decision?.reason ?? `Feature "${featureId}" not configured`,
51
+ // Only a denial is re-worded. An unbound host can still be granted the
52
+ // feature — an item ref carrying `requiredTools` mandates it at precedence 4
53
+ // with no assessment in sight — and there the source's reason is the true
54
+ // one.
55
+ reason: context.assessmentBound || granted
56
+ ? reason
57
+ : unboundAssessmentReason(featureId),
37
58
  required: Boolean(flags?.required),
38
59
  parameters: flags?.settings,
60
+ assessmentBound: context.assessmentBound,
39
61
  };
40
62
  }
@@ -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.
@@ -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
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Composition emit scheduler (PIE-885).
3
+ *
4
+ * `PieAssessmentToolkit.svelte` publishes the section composition to the
5
+ * players through exactly one path — the `composition-changed` event — and
6
+ * coalesces bursts of updates behind a one-shot latch so several changes
7
+ * within one frame produce a single emit.
8
+ *
9
+ * That latch used to be cleared only by a `requestAnimationFrame` callback,
10
+ * and the frame branch was chosen whenever `window.requestAnimationFrame`
11
+ * merely *existed* rather than when it was known to fire. In a document that
12
+ * never paints the callback never ran, the latch never cleared, and no
13
+ * `composition-changed` was ever dispatched: the section controller held a
14
+ * correct view model while the player kept its initial empty composition, so
15
+ * every `pie-section-player` route rendered no content at all. The permanent
16
+ * failure was in contexts with no compositor — headless browsers, hidden or
17
+ * offscreen tabs, agent and CI automation harnesses. A background tab only
18
+ * rendered late, because its pending frame becomes due on refocus.
19
+ *
20
+ * So the frame is raced against a deadline timer instead of trusted. Whichever
21
+ * arrives first releases the latch and flushes, and a non-painting document
22
+ * degrades to a slower render rather than a permanent blank. Svelte's own
23
+ * `tick()` races the same two primitives for the same reason.
24
+ *
25
+ * This scheduler owns the latch and both handles, which is what makes "the
26
+ * latch cannot stay set after a scheduled frame is cancelled or superseded"
27
+ * structural rather than a discipline every call site has to keep: releasing
28
+ * the latch and releasing the handles is one operation in one place.
29
+ *
30
+ * Deliberately stateless across cycles. A cycle the timer wins does not switch
31
+ * the scheduler into a timer-preferring mode: that trades the deadline cost for
32
+ * a mode which never returns to frame alignment once a single frame is missed,
33
+ * and a slower render is the accepted outcome.
34
+ *
35
+ * NOT a public surface: internal to the toolkit and not exported from
36
+ * `runtime/engine.ts`. It exists as its own module so the race can be pinned in
37
+ * a unit test without mounting the toolkit CE — same rationale as
38
+ * `runtime/stage-emit-gate.ts`.
39
+ */
40
+ /**
41
+ * How long to wait for a frame before the timer takes over.
42
+ *
43
+ * Six frames at 60fps, so on any normally painting document the frame still
44
+ * wins and emits stay paint-aligned. Chrome clamps timers in hidden tabs to
45
+ * ≥1s and can clamp harder under intensive throttling; that makes a hidden tab
46
+ * bounded-slow instead of blank, which is the point.
47
+ */
48
+ export declare const DEFAULT_FRAME_DEADLINE_MS = 100;
49
+ /**
50
+ * Timing primitives, injectable so tests can drive the race deterministically.
51
+ *
52
+ * An omitted primitive is resolved from `globalThis` at `schedule()` time
53
+ * rather than captured at construction: the pre-fix code also checked per
54
+ * emit, and a host can install a `requestAnimationFrame` shim after the
55
+ * toolkit is constructed. An explicit `null` opts the primitive out.
56
+ */
57
+ export interface CompositionEmitSchedulerTiming {
58
+ requestFrame?: ((callback: () => void) => number) | null;
59
+ cancelFrame?: ((handle: number) => void) | null;
60
+ setTimer?: ((callback: () => void, delayMs: number) => unknown) | null;
61
+ clearTimer?: ((handle: unknown) => void) | null;
62
+ queueMicrotaskFn?: ((callback: () => void) => void) | null;
63
+ frameDeadlineMs?: number;
64
+ }
65
+ export interface CompositionEmitScheduler {
66
+ /**
67
+ * Arm a flush. While a cycle is pending this only replaces the stored
68
+ * callback — that is the coalescing guarantee: one resolution, one flush.
69
+ */
70
+ schedule(flush: () => void): void;
71
+ /**
72
+ * Abandon a pending cycle. Releases the latch and both handles together,
73
+ * and is idempotent.
74
+ */
75
+ cancel(): void;
76
+ isPending(): boolean;
77
+ }
78
+ export declare function createCompositionEmitScheduler(timing?: CompositionEmitSchedulerTiming): CompositionEmitScheduler;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Composition emit scheduler (PIE-885).
3
+ *
4
+ * `PieAssessmentToolkit.svelte` publishes the section composition to the
5
+ * players through exactly one path — the `composition-changed` event — and
6
+ * coalesces bursts of updates behind a one-shot latch so several changes
7
+ * within one frame produce a single emit.
8
+ *
9
+ * That latch used to be cleared only by a `requestAnimationFrame` callback,
10
+ * and the frame branch was chosen whenever `window.requestAnimationFrame`
11
+ * merely *existed* rather than when it was known to fire. In a document that
12
+ * never paints the callback never ran, the latch never cleared, and no
13
+ * `composition-changed` was ever dispatched: the section controller held a
14
+ * correct view model while the player kept its initial empty composition, so
15
+ * every `pie-section-player` route rendered no content at all. The permanent
16
+ * failure was in contexts with no compositor — headless browsers, hidden or
17
+ * offscreen tabs, agent and CI automation harnesses. A background tab only
18
+ * rendered late, because its pending frame becomes due on refocus.
19
+ *
20
+ * So the frame is raced against a deadline timer instead of trusted. Whichever
21
+ * arrives first releases the latch and flushes, and a non-painting document
22
+ * degrades to a slower render rather than a permanent blank. Svelte's own
23
+ * `tick()` races the same two primitives for the same reason.
24
+ *
25
+ * This scheduler owns the latch and both handles, which is what makes "the
26
+ * latch cannot stay set after a scheduled frame is cancelled or superseded"
27
+ * structural rather than a discipline every call site has to keep: releasing
28
+ * the latch and releasing the handles is one operation in one place.
29
+ *
30
+ * Deliberately stateless across cycles. A cycle the timer wins does not switch
31
+ * the scheduler into a timer-preferring mode: that trades the deadline cost for
32
+ * a mode which never returns to frame alignment once a single frame is missed,
33
+ * and a slower render is the accepted outcome.
34
+ *
35
+ * NOT a public surface: internal to the toolkit and not exported from
36
+ * `runtime/engine.ts`. It exists as its own module so the race can be pinned in
37
+ * a unit test without mounting the toolkit CE — same rationale as
38
+ * `runtime/stage-emit-gate.ts`.
39
+ */
40
+ /**
41
+ * How long to wait for a frame before the timer takes over.
42
+ *
43
+ * Six frames at 60fps, so on any normally painting document the frame still
44
+ * wins and emits stay paint-aligned. Chrome clamps timers in hidden tabs to
45
+ * ≥1s and can clamp harder under intensive throttling; that makes a hidden tab
46
+ * bounded-slow instead of blank, which is the point.
47
+ */
48
+ export const DEFAULT_FRAME_DEADLINE_MS = 100;
49
+ function pickFunction(override, ambient) {
50
+ if (typeof override === "function")
51
+ return override;
52
+ if (override === null)
53
+ return null;
54
+ return typeof ambient === "function" ? ambient : null;
55
+ }
56
+ export function createCompositionEmitScheduler(timing = {}) {
57
+ const frameDeadlineMs = timing.frameDeadlineMs ?? DEFAULT_FRAME_DEADLINE_MS;
58
+ let pending = false;
59
+ let pendingFlush = null;
60
+ let frameHandle = null;
61
+ let timerHandle = null;
62
+ // Kept from the arming call so a cancel is paired with the primitive that
63
+ // actually armed the handle.
64
+ let cancelFrame = null;
65
+ let clearTimer = null;
66
+ function release() {
67
+ pending = false;
68
+ if (frameHandle !== null) {
69
+ cancelFrame?.(frameHandle);
70
+ frameHandle = null;
71
+ }
72
+ if (timerHandle !== null) {
73
+ clearTimer?.(timerHandle);
74
+ timerHandle = null;
75
+ }
76
+ cancelFrame = null;
77
+ clearTimer = null;
78
+ }
79
+ /**
80
+ * Resolve one cycle. `firedFrom` names the side that arrived so its own
81
+ * handle is dropped rather than handed back to a cancel that has nothing
82
+ * left to cancel.
83
+ *
84
+ * The latch is released before the flush callback runs, so a re-entrant
85
+ * `schedule()` from inside the flush arms a fresh cycle instead of being
86
+ * swallowed. The pre-fix code cleared its latch first for the same reason.
87
+ */
88
+ function resolve(firedFrom) {
89
+ if (!pending)
90
+ return;
91
+ if (firedFrom === "frame")
92
+ frameHandle = null;
93
+ if (firedFrom === "timer")
94
+ timerHandle = null;
95
+ const flushNow = pendingFlush;
96
+ pendingFlush = null;
97
+ release();
98
+ flushNow?.();
99
+ }
100
+ function schedule(flush) {
101
+ pendingFlush = flush;
102
+ if (pending)
103
+ return;
104
+ pending = true;
105
+ const ambient = globalThis;
106
+ const requestFrame = pickFunction(timing.requestFrame, ambient.requestAnimationFrame);
107
+ cancelFrame = pickFunction(timing.cancelFrame, ambient.cancelAnimationFrame);
108
+ const setTimer = pickFunction(timing.setTimer, ambient.setTimeout);
109
+ clearTimer = pickFunction(timing.clearTimer, ambient.clearTimeout);
110
+ if (requestFrame) {
111
+ const handle = requestFrame(() => resolve("frame"));
112
+ // A primitive that calls back synchronously has already flushed and
113
+ // released; arming anything else would leave a stray handle behind.
114
+ if (!pending)
115
+ return;
116
+ frameHandle = handle;
117
+ // The frame is not trusted to arrive. Without a timer alongside it, a
118
+ // document that never paints leaves the latch set forever.
119
+ if (setTimer) {
120
+ const deadlineHandle = setTimer(() => resolve("timer"), frameDeadlineMs);
121
+ if (pending)
122
+ timerHandle = deadlineHandle;
123
+ }
124
+ return;
125
+ }
126
+ // No frame primitive at all (SSR, non-DOM test environments). Preserve
127
+ // the microtask timing those hosts already had.
128
+ const queueMicrotaskFn = pickFunction(timing.queueMicrotaskFn, ambient.queueMicrotask);
129
+ if (queueMicrotaskFn) {
130
+ queueMicrotaskFn(() => resolve("microtask"));
131
+ return;
132
+ }
133
+ if (setTimer) {
134
+ const deadlineHandle = setTimer(() => resolve("timer"), 0);
135
+ if (pending)
136
+ timerHandle = deadlineHandle;
137
+ return;
138
+ }
139
+ // Nothing to defer with. A synchronous flush is strictly better than
140
+ // dropping the composition on the floor.
141
+ resolve("microtask");
142
+ }
143
+ function cancel() {
144
+ if (!pending)
145
+ return;
146
+ pendingFlush = null;
147
+ release();
148
+ }
149
+ return {
150
+ schedule,
151
+ cancel,
152
+ isPending: () => pending,
153
+ };
154
+ }
@@ -120,7 +120,7 @@ export declare function resolveRuntime(args: {
120
120
  createSectionController: unknown;
121
121
  isolation: string;
122
122
  env: Record<string, unknown>;
123
- toolConfigStrictness: "off" | "error" | "warn";
123
+ toolConfigStrictness: "off" | "warn" | "error";
124
124
  onFrameworkError: FrameworkErrorHandler | undefined;
125
125
  onStageChange: StageChangeHandler | undefined;
126
126
  onLoadingComplete: LoadingCompleteHandler | undefined;
@@ -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