@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
@@ -1,3 +1,4 @@
1
+ import { catalogOwnerContextFor, collectOwnerCatalogRegistrations, } from "./catalog-owner.js";
1
2
  import { sanitizeSsmlString } from "./SSMLExtractor.js";
2
3
  /**
3
4
  * The catalog types PIE names, plus the rule for the ones it does not.
@@ -39,6 +40,39 @@ export function isKnownCatalogType(type) {
39
40
  // "this token is not a thing", and repeating it per card or per lookup would bury
40
41
  // it under itself.
41
42
  const reportedUnknownTypes = new Set();
43
+ function cloneAndFreezeCatalogValue(value, seen = new Set()) {
44
+ if (value === null ||
45
+ typeof value === "string" ||
46
+ typeof value === "number" ||
47
+ typeof value === "boolean" ||
48
+ typeof value === "undefined") {
49
+ return value;
50
+ }
51
+ if (typeof value !== "object") {
52
+ throw new TypeError("catalog cards must contain JSON-compatible values");
53
+ }
54
+ if (seen.has(value)) {
55
+ throw new TypeError("catalog cards must not contain cycles");
56
+ }
57
+ seen.add(value);
58
+ try {
59
+ if (Array.isArray(value)) {
60
+ return Object.freeze(value.map((entry) => cloneAndFreezeCatalogValue(entry, seen)));
61
+ }
62
+ const prototype = Object.getPrototypeOf(value);
63
+ if (prototype !== Object.prototype && prototype !== null) {
64
+ throw new TypeError("catalog cards must contain only plain objects and arrays");
65
+ }
66
+ const cloned = {};
67
+ for (const [key, entry] of Object.entries(value)) {
68
+ cloned[key] = cloneAndFreezeCatalogValue(entry, seen);
69
+ }
70
+ return Object.freeze(cloned);
71
+ }
72
+ finally {
73
+ seen.delete(value);
74
+ }
75
+ }
42
76
  function reportUnknownCatalogType(type, side, where) {
43
77
  const key = `${side}|${type}`;
44
78
  if (reportedUnknownTypes.has(key))
@@ -93,6 +127,7 @@ export class AccessibilityCatalogResolver {
93
127
  // pass per unique string (sanitizeSsmlString is idempotent).
94
128
  sanitizedSpokenCache = new Map();
95
129
  catalogChangeListeners = new Set();
130
+ reportedSnapshotProblems = new Set();
96
131
  constructor(assessmentCatalogs, defaultLanguage = "en-US") {
97
132
  this.defaultLanguage = defaultLanguage;
98
133
  this.indexCatalogs(assessmentCatalogs ?? [], "assessment");
@@ -162,9 +197,9 @@ export class AccessibilityCatalogResolver {
162
197
  targetMap.set(catalog.identifier, catalog);
163
198
  }
164
199
  }
165
- registerCatalogs(context, catalogs) {
200
+ insertScopedCatalogs(context, catalogs) {
166
201
  if (!catalogs || catalogs.length === 0)
167
- return () => { };
202
+ return null;
168
203
  const key = this.getOwnerKey(context);
169
204
  const existing = this.scopedCatalogs.get(key);
170
205
  const scoped = existing?.catalogs ?? new Map();
@@ -177,33 +212,102 @@ export class AccessibilityCatalogResolver {
177
212
  scoped.set(catalog.identifier, catalog);
178
213
  insertedIds.push(catalog.identifier);
179
214
  }
180
- if (!existing) {
215
+ if (!existing && insertedIds.length > 0) {
181
216
  this.scopedCatalogs.set(key, {
182
217
  context: { ...context },
183
218
  catalogs: scoped,
184
219
  });
185
220
  }
221
+ return { key, context: { ...context }, catalogs: scoped, insertedIds };
222
+ }
223
+ removeScopedInsertion(insertion) {
224
+ const current = this.scopedCatalogs.get(insertion.key);
225
+ if (current?.catalogs !== insertion.catalogs)
226
+ return false;
227
+ let removed = false;
228
+ for (const insertedId of insertion.insertedIds) {
229
+ removed = current.catalogs.delete(insertedId) || removed;
230
+ }
231
+ if (current.catalogs.size === 0) {
232
+ this.scopedCatalogs.delete(insertion.key);
233
+ }
234
+ return removed;
235
+ }
236
+ registerCatalogs(context, catalogs) {
237
+ const insertion = this.insertScopedCatalogs(context, catalogs);
186
238
  // Only when something was actually inserted: an all-duplicates call changes
187
239
  // nothing, and waking every reader to re-resolve for that would make the
188
240
  // signal untrustworthy.
189
- if (insertedIds.length > 0) {
241
+ if (insertion && insertion.insertedIds.length > 0) {
190
242
  this.emitCatalogsChange({ reason: "scoped-registered", context });
191
243
  }
244
+ let active = true;
192
245
  return () => {
193
- const current = this.scopedCatalogs.get(key);
194
- if (current?.catalogs !== scoped)
246
+ if (!active)
195
247
  return;
196
- for (const insertedId of insertedIds) {
197
- current.catalogs.delete(insertedId);
248
+ active = false;
249
+ if (insertion && this.removeScopedInsertion(insertion)) {
250
+ this.emitCatalogsChange({ reason: "scoped-removed", context });
251
+ }
252
+ };
253
+ }
254
+ /**
255
+ * Register every catalog carried by one mounted item or passage as one
256
+ * owner-level transaction.
257
+ *
258
+ * The resolver owns the entity walk and emits one post-mutation signal, so a
259
+ * reader never observes only the root or only the model half of an owner.
260
+ */
261
+ registerOwner(registration) {
262
+ const { owner, entity } = registration;
263
+ const context = catalogOwnerContextFor(owner);
264
+ const insertions = [];
265
+ let registrations = [];
266
+ try {
267
+ registrations = collectOwnerCatalogRegistrations(entity, owner);
268
+ }
269
+ catch (error) {
270
+ console.warn("[AccessibilityCatalogResolver] A mounted content owner could not be traversed for catalogs; the primary content remains available.", error);
271
+ }
272
+ for (const entry of registrations) {
273
+ try {
274
+ const insertion = this.insertScopedCatalogs(entry.context, entry.catalogs);
275
+ if (insertion)
276
+ insertions.push(insertion);
198
277
  }
199
- if (current.catalogs.size === 0) {
200
- this.scopedCatalogs.delete(key);
278
+ catch (error) {
279
+ console.warn("[AccessibilityCatalogResolver] Invalid catalogs on a mounted content owner were ignored; the primary content and other valid catalogs remain available.", error);
201
280
  }
202
- if (insertedIds.length > 0) {
281
+ }
282
+ const changed = insertions.some((insertion) => insertion.insertedIds.length > 0);
283
+ if (changed) {
284
+ this.emitCatalogsChange({ reason: "scoped-registered", context });
285
+ }
286
+ let active = true;
287
+ return () => {
288
+ if (!active)
289
+ return;
290
+ active = false;
291
+ let removed = false;
292
+ for (const insertion of insertions) {
293
+ removed = this.removeScopedInsertion(insertion) || removed;
294
+ }
295
+ if (removed) {
203
296
  this.emitCatalogsChange({ reason: "scoped-removed", context });
204
297
  }
205
298
  };
206
299
  }
300
+ /** Bind catalog reads and change observation to one content owner. */
301
+ forOwner(context) {
302
+ const ownerContext = { ...context };
303
+ return Object.freeze({
304
+ snapshot: () => this.createOwnerSnapshot(ownerContext),
305
+ onChange: (listener) => this.onCatalogsChange((event) => {
306
+ if (this.catalogChangeAffectsOwner(event, ownerContext))
307
+ listener();
308
+ }),
309
+ });
310
+ }
207
311
  /**
208
312
  * Add item-level catalogs (called when rendering a new item)
209
313
  */
@@ -353,6 +457,61 @@ export class AccessibilityCatalogResolver {
353
457
  }
354
458
  return true;
355
459
  }
460
+ ownerScopedCatalogEntries(context) {
461
+ const entries = [];
462
+ const exact = this.scopedCatalogs.get(this.getOwnerKey(context));
463
+ if (exact)
464
+ entries.push(...exact.catalogs.entries());
465
+ for (const owner of this.scopedCatalogs.values()) {
466
+ if (owner === exact)
467
+ continue;
468
+ if (!this.isCompatibleOwnerContext(owner.context, context))
469
+ continue;
470
+ entries.push(...owner.catalogs.entries());
471
+ }
472
+ return entries;
473
+ }
474
+ createOwnerSnapshot(context) {
475
+ const cards = [];
476
+ const entries = this.ownerScopedCatalogEntries(context);
477
+ if (context.ownerKind === "global") {
478
+ entries.push(...this.assessmentCatalogs.entries());
479
+ }
480
+ for (const [catalogId, catalog] of entries) {
481
+ for (const card of catalog.cards) {
482
+ try {
483
+ cards.push(Object.freeze({
484
+ catalogId,
485
+ card: cloneAndFreezeCatalogValue(card),
486
+ }));
487
+ }
488
+ catch (error) {
489
+ const key = `${this.getOwnerKey(context)}\u0000${catalogId}`;
490
+ if (!this.reportedSnapshotProblems.has(key)) {
491
+ this.reportedSnapshotProblems.add(key);
492
+ console.warn(`[AccessibilityCatalogResolver] Catalog "${catalogId}" contains a card that cannot be exposed in an owner snapshot; that card was ignored.`, error);
493
+ }
494
+ }
495
+ }
496
+ }
497
+ return Object.freeze({ cards: Object.freeze(cards) });
498
+ }
499
+ catalogChangeAffectsOwner(event, context) {
500
+ if (!event.context)
501
+ return false;
502
+ if (this.isCompatibleOwnerContext(event.context, context))
503
+ return true;
504
+ // `registerOwner` emits once at the entity root after registering root and
505
+ // model catalogs transactionally. A model-bound reader belongs to that same
506
+ // owner family and must observe the root event too.
507
+ if (context.ownerKind === "itemModel" &&
508
+ context.modelId &&
509
+ event.context.ownerKind === "itemModel" &&
510
+ !event.context.modelId) {
511
+ return this.isCompatibleOwnerContext({ ...event.context, modelId: context.modelId }, context);
512
+ }
513
+ return false;
514
+ }
356
515
  // The single funnel every registration path runs through — the constructor and
357
516
  // `addItemCatalogs` by way of `indexCatalogs`, and `registerCatalogs`
358
517
  // directly — which is why the unknown-type report lives here rather than at
@@ -487,6 +646,15 @@ export class AccessibilityCatalogResolver {
487
646
  allLanguages.add(card.language);
488
647
  }
489
648
  }
649
+ for (const owner of this.scopedCatalogs.values()) {
650
+ for (const catalog of owner.catalogs.values()) {
651
+ for (const card of catalog.cards) {
652
+ allTypes.add(card.catalog);
653
+ if (card.language)
654
+ allLanguages.add(card.language);
655
+ }
656
+ }
657
+ }
490
658
  return {
491
659
  totalCatalogs: this.assessmentCatalogs.size +
492
660
  this.itemCatalogs.size +
@@ -535,6 +703,7 @@ export class AccessibilityCatalogResolver {
535
703
  this.itemCatalogs.clear();
536
704
  this.scopedCatalogs.clear();
537
705
  this.sanitizedSpokenCache.clear();
706
+ this.reportedSnapshotProblems.clear();
538
707
  }
539
708
  /**
540
709
  * Destroy and cleanup
@@ -68,6 +68,13 @@ export declare class TTSService {
68
68
  private sentenceHighlightSegments;
69
69
  private currentSeekSegmentIndex;
70
70
  private activeSentenceStartOffset;
71
+ private playbackStartDeferredRunId;
72
+ private pendingPlaybackStartHighlights;
73
+ private playbackStartBarrierCleanup;
74
+ private playbackRateWriteQueue;
75
+ private playbackRateRequestId;
76
+ private pendingPlaybackRateRequest;
77
+ private activePlaybackRate;
71
78
  private activeHighlightMode;
72
79
  private lastRenderedRegionTarget;
73
80
  private telemetryReporter;
@@ -75,6 +82,10 @@ export declare class TTSService {
75
82
  private static readonly debugEnabled;
76
83
  constructor();
77
84
  private debugLog;
85
+ private clearPlaybackStartBarrier;
86
+ private notifyPlaybackStarted;
87
+ private installPlaybackStartBarrier;
88
+ private runWhenPlaybackStarts;
78
89
  private emitTelemetry;
79
90
  /**
80
91
  * Initialize TTS service with a provider