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

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 (73) hide show
  1. package/README.md +81 -9
  2. package/dist/attempt/AssessmentSession.d.ts +7 -27
  3. package/dist/components/ItemToolBar.custom-element.js +1 -1
  4. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  5. package/dist/components/SectionToolBar.custom-element.js +1 -1
  6. package/dist/components/chunks/ItemToolBar-pe5szfyx.js +46 -0
  7. package/dist/components/chunks/ItemToolBar-rd7te9r0.js +51 -0
  8. package/dist/components/item-toolbar-element.js +1 -0
  9. package/dist/components/pie-assessment-toolkit-element.js +1 -0
  10. package/dist/components/section-toolbar-element.js +1 -0
  11. package/dist/context/assessment-toolkit-context.d.ts +28 -0
  12. package/dist/index.d.ts +7 -3
  13. package/dist/index.js +4 -2
  14. package/dist/policy/core/compose-decision.js +14 -0
  15. package/dist/policy/core/decision-types.d.ts +1 -1
  16. package/dist/policy/sources/PnpPolicySource.d.ts +8 -0
  17. package/dist/policy/sources/PnpPolicySource.js +19 -10
  18. package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
  19. package/dist/runtime/SectionRuntimeEngine.js +66 -1
  20. package/dist/runtime/core/engine-resolver.d.ts +25 -1
  21. package/dist/runtime/registration-events.d.ts +52 -0
  22. package/dist/runtime/registration-events.js +2 -0
  23. package/dist/services/AccessibilityCatalogResolver.js +21 -5
  24. package/dist/services/I18nService.d.ts +28 -100
  25. package/dist/services/I18nService.js +43 -233
  26. package/dist/services/TTSService.d.ts +12 -0
  27. package/dist/services/TTSService.js +18 -17
  28. package/dist/services/ToolRegistry.d.ts +120 -2
  29. package/dist/services/ToolRegistry.js +64 -0
  30. package/dist/services/ToolkitCoordinator.d.ts +39 -37
  31. package/dist/services/ToolkitCoordinator.js +40 -0
  32. package/dist/services/audio-handoff.d.ts +39 -0
  33. package/dist/services/audio-handoff.js +58 -0
  34. package/dist/services/catalog-media.d.ts +35 -4
  35. package/dist/services/catalog-media.js +92 -3
  36. package/dist/services/framework-error.d.ts +15 -1
  37. package/dist/services/interfaces.d.ts +28 -0
  38. package/dist/services/pnp-standard-features.d.ts +1 -1
  39. package/dist/services/section-controller-types.d.ts +218 -7
  40. package/dist/services/selection-action.d.ts +49 -0
  41. package/dist/services/selection-action.js +10 -0
  42. package/dist/services/spoken-audio-cards.js +5 -1
  43. package/dist/services/tool-context.d.ts +6 -5
  44. package/dist/services/tool-context.js +205 -155
  45. package/dist/services/tool-icons.d.ts +18 -0
  46. package/dist/services/tool-icons.js +31 -0
  47. package/dist/services/tool-providers/CortexToolProvider.d.ts +18 -0
  48. package/dist/services/tool-providers/CortexToolProvider.js +32 -0
  49. package/dist/services/tool-providers/DesmosToolProvider.d.ts +13 -101
  50. package/dist/services/tool-providers/DesmosToolProvider.js +14 -145
  51. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +21 -0
  52. package/dist/services/tool-providers/GeoGebraToolProvider.js +32 -0
  53. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +35 -0
  54. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +95 -0
  55. package/dist/services/tool-providers/index.d.ts +4 -0
  56. package/dist/services/tool-providers/index.js +2 -0
  57. package/dist/services/tool-request.d.ts +106 -0
  58. package/dist/services/tool-request.js +127 -0
  59. package/dist/services/toolbar-items.d.ts +6 -0
  60. package/dist/tools/client.d.ts +0 -2
  61. package/dist/tools/client.js +0 -4
  62. package/dist/tools/internal.d.ts +7 -1
  63. package/dist/tools/internal.js +8 -1
  64. package/dist/tools/tool-surface-host.d.ts +57 -0
  65. package/dist/tools/tool-surface-host.js +610 -0
  66. package/dist/tools/types.d.ts +1 -66
  67. package/package.json +22 -12
  68. package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
  69. package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
  70. package/dist/tools/calculators/desmos-provider.d.ts +0 -46
  71. package/dist/tools/calculators/desmos-provider.js +0 -393
  72. package/dist/tools/library-loader.d.ts +0 -62
  73. package/dist/tools/library-loader.js +0 -261
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Headless host for capabilities that render into a renderer's surfaces.
3
+ *
4
+ * The interface deliberately exposes only current input, a two-boolean snapshot,
5
+ * and teardown. Discovery, policy/catalog invalidation, content resolution, lazy
6
+ * loading, DOM reconciliation, error isolation, and registry observation stay
7
+ * inside this module. Svelte callers are geometry adapters over this seam.
8
+ *
9
+ * It lives here rather than in `section-player`, where it was written, because a
10
+ * second renderer now opens a surface: the annotation toolbar hosts the
11
+ * capabilities that act on a text selection. Two copies of mount/reconcile/
12
+ * registry-observation would drift, and the drift would be invisible until one
13
+ * renderer stopped honouring the grant-AND-content rule. Nothing about the module
14
+ * was section-shaped — it already imported only from this package — so the move
15
+ * is a relocation, not a rewrite. The one thing that *was* section-shaped is the
16
+ * name it reported errors under, which is now {@link ToolSurfaceHostOptions.hostLabel}.
17
+ */
18
+ import { isHostDeniedFeature } from "../policy/core/feature-decision.js";
19
+ import { toFrameworkErrorModel } from "../services/framework-error.js";
20
+ import { resolveContentCapabilities } from "./content-capability-resolution.js";
21
+ const SECTION_POLICY_LEVELS = ["section", "item", "passage"];
22
+ function stableSerializableJson(value) {
23
+ const seen = new Set();
24
+ function normalize(current, path) {
25
+ if (current === null ||
26
+ typeof current === "string" ||
27
+ typeof current === "boolean") {
28
+ return current;
29
+ }
30
+ if (typeof current === "number") {
31
+ return Number.isFinite(current) ? current : null;
32
+ }
33
+ if (typeof current === "undefined") {
34
+ return undefined;
35
+ }
36
+ if (typeof current === "function" || typeof current === "symbol") {
37
+ throw new TypeError(`${path} is not JSON-serializable`);
38
+ }
39
+ if (typeof current === "bigint") {
40
+ throw new TypeError(`${path} is not JSON-serializable`);
41
+ }
42
+ if (typeof current !== "object") {
43
+ throw new TypeError(`${path} is not JSON-serializable`);
44
+ }
45
+ if (seen.has(current)) {
46
+ throw new TypeError(`${path} contains a cycle`);
47
+ }
48
+ seen.add(current);
49
+ try {
50
+ if (Array.isArray(current)) {
51
+ return current.map((entry, index) => normalize(entry, `${path}[${index}]`) ?? null);
52
+ }
53
+ const prototype = Object.getPrototypeOf(current);
54
+ if (prototype !== Object.prototype && prototype !== null) {
55
+ throw new TypeError(`${path} must contain only plain objects and arrays`);
56
+ }
57
+ const record = current;
58
+ const normalized = {};
59
+ for (const key of Object.keys(record).sort()) {
60
+ const entry = normalize(record[key], `${path}.${key}`);
61
+ if (entry !== undefined)
62
+ normalized[key] = entry;
63
+ }
64
+ return normalized;
65
+ }
66
+ finally {
67
+ seen.delete(current);
68
+ }
69
+ }
70
+ const serialized = JSON.stringify(normalize(value, "content"));
71
+ if (serialized === undefined) {
72
+ throw new TypeError("content is not JSON-serializable");
73
+ }
74
+ return serialized;
75
+ }
76
+ function scopeEquals(left, right) {
77
+ if (left.kind !== right.kind)
78
+ return false;
79
+ if (left.kind === "section" && right.kind === "section") {
80
+ return (left.assessmentId === right.assessmentId &&
81
+ left.sectionId === right.sectionId);
82
+ }
83
+ if (left.kind === "content" && right.kind === "content") {
84
+ const leftOwner = left.ownerContext;
85
+ const rightOwner = right.ownerContext;
86
+ return (leftOwner.ownerKind === rightOwner.ownerKind &&
87
+ leftOwner.assessmentId === rightOwner.assessmentId &&
88
+ leftOwner.sectionId === rightOwner.sectionId &&
89
+ leftOwner.canonicalItemId === rightOwner.canonicalItemId &&
90
+ leftOwner.itemId === rightOwner.itemId &&
91
+ leftOwner.passageId === rightOwner.passageId &&
92
+ leftOwner.modelId === rightOwner.modelId);
93
+ }
94
+ return false;
95
+ }
96
+ function inputEquals(left, right) {
97
+ return (left.anchor === right.anchor &&
98
+ left.surface === right.surface &&
99
+ left.registry === right.registry &&
100
+ left.services.toolkitCoordinator === right.services.toolkitCoordinator &&
101
+ left.services.ttsService === right.services.ttsService &&
102
+ left.services.catalogResolver === right.services.catalogResolver &&
103
+ scopeEquals(left.scope, right.scope));
104
+ }
105
+ function subscriptionInputsEqual(left, right) {
106
+ return (left.registry === right.registry &&
107
+ left.services.toolkitCoordinator === right.services.toolkitCoordinator &&
108
+ left.services.catalogResolver === right.services.catalogResolver &&
109
+ scopeEquals(left.scope, right.scope));
110
+ }
111
+ function contextSignature(context) {
112
+ try {
113
+ return stableSerializableJson({
114
+ toolId: context.toolId,
115
+ featureId: context.featureId,
116
+ surface: context.surface,
117
+ parameters: context.parameters ?? null,
118
+ content: context.content ?? null,
119
+ });
120
+ }
121
+ catch {
122
+ // Parameters are host/provider data and are not required to be serializable.
123
+ // Fall back to syncing them on each invalidation instead of rejecting the tool.
124
+ return null;
125
+ }
126
+ }
127
+ function sameContext(mountedEntry, nextEntry) {
128
+ if (mountedEntry.contextSignature === null ||
129
+ nextEntry.contextSignature === null ||
130
+ mountedEntry.contextSignature !== nextEntry.contextSignature) {
131
+ return false;
132
+ }
133
+ return (mountedEntry.context.services.toolkitCoordinator ===
134
+ nextEntry.context.services.toolkitCoordinator &&
135
+ mountedEntry.context.services.ttsService ===
136
+ nextEntry.context.services.ttsService &&
137
+ mountedEntry.context.services.catalogResolver ===
138
+ nextEntry.context.services.catalogResolver);
139
+ }
140
+ /** Create one lifecycle owner for one host surface anchor. */
141
+ export function createToolSurfaceHost(onSnapshot, options) {
142
+ const { hostLabel } = options;
143
+ let input = null;
144
+ let destroyed = false;
145
+ let snapshot = { mountable: false, occupied: false };
146
+ let unsubscribeRegistry = null;
147
+ let unsubscribePolicy = null;
148
+ let unsubscribeCatalogs = null;
149
+ let catalogOwnerView = null;
150
+ const mounted = new Map();
151
+ const loadedRegistrations = new Map();
152
+ const pendingLoads = new Map();
153
+ const reportedWarnings = new Set();
154
+ function publish(next) {
155
+ if (next.mountable === snapshot.mountable &&
156
+ next.occupied === snapshot.occupied) {
157
+ return;
158
+ }
159
+ snapshot = next;
160
+ onSnapshot({ ...next });
161
+ }
162
+ function report(toolId, phase, message, cause) {
163
+ const current = input;
164
+ const key = `${current?.surface ?? "unknown"}\u0000${toolId}\u0000${phase}\u0000${message}`;
165
+ if (reportedWarnings.has(key))
166
+ return;
167
+ reportedWarnings.add(key);
168
+ const model = toFrameworkErrorModel({
169
+ kind: "tool-surface",
170
+ severity: "warning",
171
+ source: `${hostLabel}/tool-surface-host`,
172
+ message,
173
+ details: [
174
+ `surface=${current?.surface ?? "unknown"}`,
175
+ `toolId=${toolId}`,
176
+ `phase=${phase}`,
177
+ ],
178
+ recoverable: true,
179
+ cause,
180
+ });
181
+ const coordinator = current?.services.toolkitCoordinator;
182
+ if (typeof coordinator?.reportFrameworkError === "function") {
183
+ try {
184
+ coordinator.reportFrameworkError(model);
185
+ return;
186
+ }
187
+ catch (error) {
188
+ console.warn(`[${hostLabel}] tool surface warning reporter failed:`, error);
189
+ }
190
+ }
191
+ if (current?.anchor && typeof CustomEvent === "function") {
192
+ current.anchor.dispatchEvent(new CustomEvent("framework-error", {
193
+ detail: model,
194
+ bubbles: true,
195
+ composed: true,
196
+ }));
197
+ return;
198
+ }
199
+ console.warn(`[${hostLabel}] ${message}`, cause);
200
+ }
201
+ function unmount(toolId) {
202
+ const entry = mounted.get(toolId);
203
+ if (!entry)
204
+ return;
205
+ mounted.delete(toolId);
206
+ try {
207
+ entry.destroy?.();
208
+ }
209
+ catch (error) {
210
+ report(toolId, "destroy", `Tool "${toolId}" failed while leaving the "${input?.surface ?? "unknown"}" surface.`, error);
211
+ }
212
+ finally {
213
+ try {
214
+ entry.element.remove();
215
+ }
216
+ catch (error) {
217
+ // A custom element can override `remove`; use its parent as a fallback so
218
+ // one broken teardown hook cannot strand the node or abort reconciliation.
219
+ try {
220
+ entry.element.parentNode?.removeChild(entry.element);
221
+ }
222
+ catch {
223
+ // The warning below remains observable even if the DOM itself rejects
224
+ // both removal paths.
225
+ }
226
+ report(toolId, "destroy", `Tool "${toolId}" failed DOM removal from the "${input?.surface ?? "unknown"}" surface.`, error);
227
+ }
228
+ }
229
+ }
230
+ function unmountAll() {
231
+ for (const toolId of [...mounted.keys()])
232
+ unmount(toolId);
233
+ }
234
+ function contentEligibility(current, registration, catalogs) {
235
+ if (current.scope.kind !== "content")
236
+ return null;
237
+ const coordinator = current.services.toolkitCoordinator;
238
+ const [resolved] = resolveContentCapabilities({
239
+ registrations: [registration],
240
+ catalogs,
241
+ // One decision per feature id, in the rule's three states. The scan across
242
+ // a capability's support ids, the gate-only probe of its tool id, and
243
+ // denial's precedence over both a grant and `resolvesWithoutGrant` live in
244
+ // the rule, so this host and print cannot answer differently. All this
245
+ // adapter owns is reading a `FeaturePolicyDecision`: `granted` is not
246
+ // enough on its own, because a host gate and an unconfigured feature are
247
+ // both `granted: false` and only one of them may be reopened by content.
248
+ policyFor: (featureId) => {
249
+ const decision = coordinator?.decideFeaturePolicy?.(featureId);
250
+ if (isHostDeniedFeature(decision))
251
+ return { outcome: "denied" };
252
+ if (decision?.granted === true) {
253
+ return {
254
+ outcome: "granted",
255
+ featureId,
256
+ parameters: decision.parameters,
257
+ };
258
+ }
259
+ return { outcome: "silent" };
260
+ },
261
+ onError: (failed, phase, error) => {
262
+ report(failed.toolId, "resolve", phase === "policy"
263
+ ? `Tool "${failed.toolId}" failed policy resolution for the "${current.surface}" surface.`
264
+ : `Tool "${failed.toolId}" failed authored-content resolution for the "${current.surface}" surface.`, error);
265
+ },
266
+ });
267
+ if (!resolved)
268
+ return null;
269
+ // Serializability is this host's requirement rather than the rule's: it
270
+ // re-resolves on every policy and catalog signal and compares the answer
271
+ // structurally, so content it cannot compare would report a change on every
272
+ // invalidation. Print resolves once and does not care.
273
+ if (resolved.content !== null) {
274
+ try {
275
+ stableSerializableJson(resolved.content);
276
+ }
277
+ catch (error) {
278
+ report(registration.toolId, "resolve", `Tool "${registration.toolId}" failed authored-content resolution for the "${current.surface}" surface.`, error);
279
+ return null;
280
+ }
281
+ }
282
+ const context = {
283
+ toolId: registration.toolId,
284
+ featureId: resolved.featureId,
285
+ surface: current.surface,
286
+ parameters: resolved.parameters,
287
+ content: resolved.content,
288
+ services: current.services,
289
+ };
290
+ return {
291
+ registration,
292
+ context,
293
+ contextSignature: contextSignature(context),
294
+ };
295
+ }
296
+ function sectionEligibility(current, registration) {
297
+ if (current.scope.kind !== "section")
298
+ return null;
299
+ if (registration.requiresAuthoredContent) {
300
+ report(registration.toolId, "resolve", `Tool "${registration.toolId}" declares authored content that the section-scoped "${current.surface}" surface cannot resolve.`);
301
+ return null;
302
+ }
303
+ const coordinator = current.services.toolkitCoordinator;
304
+ if (!coordinator)
305
+ return null;
306
+ const supportIds = registration.pnpSupportIds?.length
307
+ ? registration.pnpSupportIds
308
+ : [registration.toolId];
309
+ let featureId = "";
310
+ let parameters;
311
+ try {
312
+ if (registration.activation === "region") {
313
+ for (const supportId of supportIds) {
314
+ const decision = coordinator.decideFeaturePolicy?.(supportId);
315
+ if (decision?.granted !== true)
316
+ continue;
317
+ featureId = supportId;
318
+ parameters = decision.parameters;
319
+ break;
320
+ }
321
+ if (!featureId)
322
+ return null;
323
+ }
324
+ else {
325
+ const scopeId = current.scope.sectionId || "*";
326
+ for (const level of SECTION_POLICY_LEVELS) {
327
+ const decision = coordinator.decideToolPolicy({
328
+ level,
329
+ scope: {
330
+ level,
331
+ scopeId,
332
+ assessmentId: current.scope.assessmentId,
333
+ sectionId: current.scope.sectionId || undefined,
334
+ },
335
+ });
336
+ const entry = decision.visibleTools.find((candidate) => candidate.toolId === registration.toolId);
337
+ if (!entry)
338
+ continue;
339
+ featureId = supportIds[0] ?? registration.toolId;
340
+ parameters = entry.settings;
341
+ break;
342
+ }
343
+ if (!featureId)
344
+ return null;
345
+ }
346
+ }
347
+ catch (error) {
348
+ report(registration.toolId, "resolve", `Tool "${registration.toolId}" failed policy resolution for the "${current.surface}" surface.`, error);
349
+ return null;
350
+ }
351
+ const context = {
352
+ toolId: registration.toolId,
353
+ featureId,
354
+ surface: current.surface,
355
+ parameters,
356
+ services: current.services,
357
+ };
358
+ return {
359
+ registration,
360
+ context,
361
+ contextSignature: contextSignature(context),
362
+ };
363
+ }
364
+ function resolveEligible(current) {
365
+ const registry = current.registry;
366
+ if (!registry || !current.surface)
367
+ return [];
368
+ let registrations;
369
+ try {
370
+ registrations = registry.getToolsBySurface(current.surface);
371
+ }
372
+ catch (error) {
373
+ report("registry", "resolve", `The tool registry failed discovery for the "${current.surface}" surface.`, error);
374
+ return [];
375
+ }
376
+ let catalogs = null;
377
+ if (current.scope.kind === "content" &&
378
+ catalogOwnerView &&
379
+ registrations.some((registration) => registration.requiresAuthoredContent)) {
380
+ try {
381
+ catalogs = catalogOwnerView.snapshot();
382
+ }
383
+ catch (error) {
384
+ report("catalog-owner", "resolve", `Catalog owner resolution failed for the "${current.surface}" surface; catalog-dependent capabilities were omitted.`, error);
385
+ }
386
+ }
387
+ return registrations.flatMap((registration) => {
388
+ const eligible = current.scope.kind === "content"
389
+ ? contentEligibility(current, registration, catalogs)
390
+ : sectionEligibility(current, registration);
391
+ return eligible ? [eligible] : [];
392
+ });
393
+ }
394
+ function ensureLoaded(current, entry) {
395
+ const registry = current.registry;
396
+ if (!registry)
397
+ return false;
398
+ const toolId = entry.registration.toolId;
399
+ if (loadedRegistrations.get(toolId) === entry.registration)
400
+ return true;
401
+ const existing = pendingLoads.get(toolId);
402
+ if (existing?.registry === registry &&
403
+ existing.registration === entry.registration) {
404
+ return false;
405
+ }
406
+ const promise = registry.ensureToolModuleLoaded(toolId);
407
+ const pending = { registry, registration: entry.registration };
408
+ pendingLoads.set(toolId, pending);
409
+ void promise
410
+ .then(() => {
411
+ if (pendingLoads.get(toolId) === pending)
412
+ pendingLoads.delete(toolId);
413
+ if (destroyed || input?.registry !== registry)
414
+ return;
415
+ if (registry.get(toolId) !== entry.registration)
416
+ return;
417
+ loadedRegistrations.set(toolId, entry.registration);
418
+ reconcile();
419
+ })
420
+ .catch((error) => {
421
+ if (pendingLoads.get(toolId) === pending)
422
+ pendingLoads.delete(toolId);
423
+ if (destroyed || input?.registry !== registry)
424
+ return;
425
+ if (registry.get(toolId) !== entry.registration)
426
+ return;
427
+ report(toolId, "load", `Tool "${toolId}" failed to load for the "${current.surface}" surface.`, error);
428
+ });
429
+ return false;
430
+ }
431
+ function mount(current, entry) {
432
+ const registry = current.registry;
433
+ const anchor = current.anchor;
434
+ if (!registry || !anchor)
435
+ return;
436
+ const toolId = entry.registration.toolId;
437
+ let rendered = null;
438
+ try {
439
+ rendered = registry.renderForSurface(toolId, entry.context);
440
+ if (!rendered?.element)
441
+ return;
442
+ if (rendered.ariaLabel) {
443
+ rendered.element.setAttribute("aria-label", rendered.ariaLabel);
444
+ }
445
+ anchor.appendChild(rendered.element);
446
+ mounted.set(toolId, {
447
+ registration: entry.registration,
448
+ element: rendered.element,
449
+ sync: rendered.sync,
450
+ destroy: rendered.destroy,
451
+ context: entry.context,
452
+ contextSignature: entry.contextSignature,
453
+ });
454
+ }
455
+ catch (error) {
456
+ if (rendered?.element.parentNode === anchor) {
457
+ try {
458
+ anchor.removeChild(rendered.element);
459
+ }
460
+ catch {
461
+ // Reporting the render failure is still preferable to escaping and
462
+ // blocking the player when the capability returned an invalid node.
463
+ }
464
+ }
465
+ report(toolId, "render", `Tool "${toolId}" failed to render into the "${current.surface}" surface.`, error);
466
+ return;
467
+ }
468
+ }
469
+ function reconcile() {
470
+ if (destroyed || !input)
471
+ return;
472
+ const current = input;
473
+ const eligible = resolveEligible(current);
474
+ const wanted = new Map(eligible.map((entry) => [entry.registration.toolId, entry]));
475
+ for (const [toolId, mountedEntry] of [...mounted]) {
476
+ const next = wanted.get(toolId);
477
+ if (!current.anchor ||
478
+ !next ||
479
+ next.registration !== mountedEntry.registration) {
480
+ unmount(toolId);
481
+ }
482
+ }
483
+ if (current.anchor && current.registry) {
484
+ for (const entry of eligible) {
485
+ const toolId = entry.registration.toolId;
486
+ const existing = mounted.get(toolId);
487
+ if (existing) {
488
+ if (sameContext(existing, entry))
489
+ continue;
490
+ try {
491
+ existing.sync?.(entry.context);
492
+ existing.context = entry.context;
493
+ existing.contextSignature = entry.contextSignature;
494
+ }
495
+ catch (error) {
496
+ report(toolId, "sync", `Tool "${toolId}" failed to synchronize in the "${current.surface}" surface.`, error);
497
+ }
498
+ continue;
499
+ }
500
+ if (ensureLoaded(current, entry))
501
+ mount(current, entry);
502
+ }
503
+ // Re-appending existing nodes is identity-preserving and keeps registry
504
+ // order even when lazy modules resolve in a different order.
505
+ for (const entry of eligible) {
506
+ const mountedEntry = mounted.get(entry.registration.toolId);
507
+ if (!mountedEntry)
508
+ continue;
509
+ try {
510
+ current.anchor.appendChild(mountedEntry.element);
511
+ }
512
+ catch (error) {
513
+ report(entry.registration.toolId, "sync", `Tool "${entry.registration.toolId}" failed DOM reconciliation in the "${current.surface}" surface.`, error);
514
+ }
515
+ }
516
+ }
517
+ publish({
518
+ mountable: eligible.length > 0,
519
+ occupied: Boolean(current.anchor && mounted.size > 0),
520
+ });
521
+ }
522
+ function invalidate() {
523
+ if (destroyed)
524
+ return;
525
+ reportedWarnings.clear();
526
+ reconcile();
527
+ }
528
+ function handleRegistryChange(event) {
529
+ if (event.kind === "module-loaders") {
530
+ for (const toolId of event.toolIds)
531
+ loadedRegistrations.delete(toolId);
532
+ }
533
+ if (event.kind === "component-overrides") {
534
+ unmountAll();
535
+ }
536
+ invalidate();
537
+ }
538
+ function detachSubscriptions() {
539
+ for (const unsubscribe of [
540
+ unsubscribeRegistry,
541
+ unsubscribePolicy,
542
+ unsubscribeCatalogs,
543
+ ]) {
544
+ try {
545
+ unsubscribe?.();
546
+ }
547
+ catch {
548
+ // The dependency may already have been disposed.
549
+ }
550
+ }
551
+ unsubscribeRegistry = null;
552
+ unsubscribePolicy = null;
553
+ unsubscribeCatalogs = null;
554
+ catalogOwnerView = null;
555
+ }
556
+ function bindSubscriptions(current) {
557
+ detachSubscriptions();
558
+ unsubscribeRegistry =
559
+ typeof current.registry?.onRegistryChange === "function"
560
+ ? current.registry.onRegistryChange(handleRegistryChange)
561
+ : null;
562
+ const coordinator = current.services.toolkitCoordinator;
563
+ if (coordinator) {
564
+ unsubscribePolicy =
565
+ typeof coordinator.onPolicyChange === "function"
566
+ ? coordinator.onPolicyChange(invalidate)
567
+ : null;
568
+ }
569
+ if (current.scope.kind === "content" && current.services.catalogResolver) {
570
+ catalogOwnerView = current.services.catalogResolver.forOwner(current.scope.ownerContext);
571
+ unsubscribeCatalogs = catalogOwnerView.onChange(invalidate);
572
+ }
573
+ }
574
+ return {
575
+ update(nextInput) {
576
+ if (destroyed)
577
+ return;
578
+ const previous = input;
579
+ let changed = true;
580
+ try {
581
+ changed = previous ? !inputEquals(previous, nextInput) : true;
582
+ }
583
+ catch {
584
+ // Owner contexts are expected to be plain serializable records. Treat an
585
+ // invalid one as changed so it cannot freeze the lifecycle.
586
+ changed = true;
587
+ }
588
+ const subscriptionsChanged = previous
589
+ ? !subscriptionInputsEqual(previous, nextInput)
590
+ : true;
591
+ input = nextInput;
592
+ if (subscriptionsChanged)
593
+ bindSubscriptions(nextInput);
594
+ if (!changed)
595
+ return;
596
+ reportedWarnings.clear();
597
+ reconcile();
598
+ },
599
+ destroy() {
600
+ if (destroyed)
601
+ return;
602
+ detachSubscriptions();
603
+ unmountAll();
604
+ destroyed = true;
605
+ input = null;
606
+ pendingLoads.clear();
607
+ loadedRegistrations.clear();
608
+ },
609
+ };
610
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool system type definitions
3
3
  */
4
- export type { CalculationHistoryEntry, Calculator, CalculatorProvider, CalculatorProviderCapabilities, CalculatorProviderConfig, CalculatorState, CalculatorType, DesmosCalculatorConfig, } from "@pie-players/pie-calculator";
4
+ export type { CalculationHistoryEntry, Calculator, CalculatorProvider, CalculatorProviderCapabilities, CalculatorProviderConfig, CalculatorState, CalculatorType, } from "@pie-players/pie-calculator";
5
5
  export type ToolId = string;
6
6
  export interface ToolState {
7
7
  id: ToolId;
@@ -148,41 +148,6 @@ export interface ResolvedItemConfig {
148
148
  export interface ResolvedItemToolParameters extends ItemToolParameters {
149
149
  finalConfig: ToolConfig;
150
150
  }
151
- /**
152
- * Library configuration for external dependencies
153
- */
154
- export interface LibraryConfig {
155
- id: string;
156
- url: string;
157
- fallbackUrls?: string[];
158
- globalVar?: string;
159
- dependencies?: string[];
160
- integrity?: string;
161
- crossorigin?: "anonymous" | "use-credentials";
162
- timeout?: number;
163
- async?: boolean;
164
- defer?: boolean;
165
- retry?: RetryConfig;
166
- }
167
- /**
168
- * Retry configuration for library loading
169
- */
170
- export interface RetryConfig {
171
- maxAttempts: number;
172
- delay: number;
173
- backoffMultiplier?: number;
174
- }
175
- /**
176
- * Library loader statistics
177
- */
178
- export interface LoaderStats {
179
- loaded: string[];
180
- failed: string[];
181
- pending: string[];
182
- cacheHits: number;
183
- cacheMisses: number;
184
- totalLoadTime: number;
185
- }
186
151
  /**
187
152
  * Base interface that all tool components should implement
188
153
  */
@@ -274,36 +239,6 @@ export interface VariantResolver {
274
239
  */
275
240
  resolveVariant(itemConfig: ItemToolConfig, context: VariantContext): ResolvedItemConfig;
276
241
  }
277
- /**
278
- * Library Loader Service
279
- * Dynamically loads external libraries with retry and fallback support
280
- */
281
- export interface LibraryLoader {
282
- /**
283
- * Load a JavaScript library
284
- */
285
- loadScript(library: LibraryConfig): Promise<void>;
286
- /**
287
- * Load a stylesheet
288
- */
289
- loadStylesheet(url: string, targetRoot?: Document | ShadowRoot): Promise<void>;
290
- /**
291
- * Check if a library is already loaded
292
- */
293
- isLoaded(libraryId: string): boolean;
294
- /**
295
- * Preload multiple libraries in parallel
296
- */
297
- preload(libraries: LibraryConfig[]): Promise<void>;
298
- /**
299
- * Unload a library (remove script tag)
300
- */
301
- unload(libraryId: string): void;
302
- /**
303
- * Get loader statistics
304
- */
305
- getStats(): LoaderStats;
306
- }
307
242
  /**
308
243
  * PIE Response Component Interface
309
244
  * For tool-to-response integration (e.g., calculator inserting into text field)