@pie-players/pie-players-shared 0.3.74 → 0.3.75
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.
- package/README.md +3 -1
- package/dist/i18n/messages/en-US.d.ts +1 -24
- package/dist/i18n/messages/en-US.js +1 -24
- package/dist/i18n/messages/nl-NL.d.ts +1 -24
- package/dist/i18n/messages/nl-NL.js +1 -24
- package/dist/i18n/types.d.ts +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/loaders/esm-adapter.d.ts +11 -0
- package/dist/loaders/esm-adapter.js +26 -8
- package/dist/loaders/index.d.ts +4 -2
- package/dist/loaders/index.js +2 -1
- package/dist/loaders/mathjax-events.d.ts +22 -0
- package/dist/loaders/mathjax-events.js +65 -0
- package/dist/loaders/preloaded-registration.d.ts +18 -2
- package/dist/loaders/preloaded-registration.js +14 -2
- package/dist/pie/elements-rendered.d.ts +25 -0
- package/dist/pie/elements-rendered.js +96 -0
- package/dist/pie/index.d.ts +4 -6
- package/dist/pie/index.js +4 -7
- package/dist/pie/instrumentation-event-bridge.js +23 -1
- package/dist/pie/instrumentation-event-map.js +11 -18
- package/dist/pie/item-controller.js +4 -0
- package/dist/pie/logger.d.ts +6 -0
- package/dist/pie/logger.js +10 -0
- package/dist/pie/math-assets.d.ts +48 -0
- package/dist/pie/math-assets.js +81 -0
- package/dist/pie/scoring.d.ts +1 -6
- package/dist/pie/scoring.js +3 -8
- package/dist/pie/session-commit.js +1 -1
- package/dist/pie/stages.d.ts +10 -47
- package/dist/pie/stages.js +10 -47
- package/dist/security/sanitize-item-markup.d.ts +4 -0
- package/dist/security/sanitize-item-markup.js +48 -2
- package/dist/security/wrap-overwide-images.js +19 -0
- package/dist/security/wrap-overwide.d.ts +8 -0
- package/dist/security/wrap-overwide.js +47 -3
- package/dist/types/index.d.ts +37 -112
- package/package.json +4 -3
- package/dist/loaders/mathjax-version-conflict.d.ts +0 -17
- package/dist/loaders/mathjax-version-conflict.js +0 -51
- package/dist/pie/stage-tracker.d.ts +0 -50
- package/dist/pie/stage-tracker.js +0 -105
|
@@ -40,9 +40,18 @@ function isInsidePieCustomElement(node, root) {
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Wrappers whose layout class was read from the live cascade. A wrapper built
|
|
45
|
+
* off-document saw only its node's inline style, so the first live pass to meet
|
|
46
|
+
* it reads the layout again; this set keeps that to once per wrapper.
|
|
47
|
+
*/
|
|
48
|
+
const liveLayoutRead = new WeakSet();
|
|
43
49
|
/**
|
|
44
50
|
* Wrap every unwrapped match under `root`. Returns the number newly wrapped so
|
|
45
51
|
* callers can short-circuit when nothing changed. Idempotent.
|
|
52
|
+
*
|
|
53
|
+
* Under a connected `root`, a wrapper whose layout was never read live gains
|
|
54
|
+
* the layout class this pass would have given it.
|
|
46
55
|
*/
|
|
47
56
|
export function wrapOverwideInElement(root, spec, options = {}) {
|
|
48
57
|
const { skipPieDescendants = false } = options;
|
|
@@ -52,24 +61,59 @@ export function wrapOverwideInElement(root, spec, options = {}) {
|
|
|
52
61
|
const ownerDocument = root.ownerDocument;
|
|
53
62
|
if (!ownerDocument)
|
|
54
63
|
return 0;
|
|
55
|
-
|
|
64
|
+
const readsLiveLayout = !!spec.layoutClass && !!ownerDocument.defaultView && root.isConnected;
|
|
65
|
+
const targets = [];
|
|
66
|
+
const unread = [];
|
|
56
67
|
for (const node of nodes) {
|
|
57
68
|
const parent = node.parentElement;
|
|
58
69
|
if (!parent)
|
|
59
70
|
continue;
|
|
60
71
|
// Idempotency — already wrapped.
|
|
61
|
-
|
|
72
|
+
const wrapped = parent.classList?.contains(spec.wrapperClass);
|
|
73
|
+
if (wrapped && (!readsLiveLayout || liveLayoutRead.has(parent)))
|
|
62
74
|
continue;
|
|
63
75
|
// Authored-markup pass: leave PIE custom-element internals alone.
|
|
64
76
|
if (skipPieDescendants && isInsidePieCustomElement(node, root))
|
|
65
77
|
continue;
|
|
66
78
|
if (node.closest(EDITING_HOST_SELECTOR))
|
|
67
79
|
continue;
|
|
80
|
+
if (!wrapped)
|
|
81
|
+
targets.push(node);
|
|
82
|
+
else if (!unread.some((other) => other.parentElement === parent))
|
|
83
|
+
unread.push(node);
|
|
84
|
+
}
|
|
85
|
+
// Layout is read for every node before the first insertion: a read after a
|
|
86
|
+
// wrap forces a fresh style resolution, one per node. A wrapper's own styles
|
|
87
|
+
// override its node's layout — the theme makes every wrapped image a block —
|
|
88
|
+
// so an existing wrapper is read with its classes lifted.
|
|
89
|
+
const unreadWrappers = unread.map((node) => node.parentElement);
|
|
90
|
+
const liftedClasses = unreadWrappers.map((wrapper) => wrapper.className);
|
|
91
|
+
for (const wrapper of unreadWrappers)
|
|
92
|
+
wrapper.className = "";
|
|
93
|
+
const layoutClasses = targets.map((node) => spec.layoutClass?.(node) ?? null);
|
|
94
|
+
const unreadLayoutClasses = unread.map((node) => spec.layoutClass?.(node) ?? null);
|
|
95
|
+
for (const [index, wrapper] of unreadWrappers.entries()) {
|
|
96
|
+
wrapper.className = liftedClasses[index];
|
|
97
|
+
const layoutClass = unreadLayoutClasses[index];
|
|
98
|
+
if (layoutClass)
|
|
99
|
+
wrapper.classList.add(layoutClass);
|
|
100
|
+
liveLayoutRead.add(wrapper);
|
|
101
|
+
}
|
|
102
|
+
let wrapped = 0;
|
|
103
|
+
for (const [index, node] of targets.entries()) {
|
|
104
|
+
const parent = node.parentElement;
|
|
105
|
+
if (!parent)
|
|
106
|
+
continue;
|
|
68
107
|
const wrapper = ownerDocument.createElement(spec.wrapperTag);
|
|
69
|
-
|
|
108
|
+
const layoutClass = layoutClasses[index];
|
|
109
|
+
wrapper.className = layoutClass
|
|
110
|
+
? `${spec.wrapperClass} ${layoutClass}`
|
|
111
|
+
: spec.wrapperClass;
|
|
70
112
|
wrapper.setAttribute("tabindex", "0");
|
|
71
113
|
wrapper.setAttribute("role", "region");
|
|
72
114
|
wrapper.setAttribute("aria-label", spec.buildAriaLabel(node));
|
|
115
|
+
if (readsLiveLayout)
|
|
116
|
+
liveLayoutRead.add(wrapper);
|
|
73
117
|
parent.insertBefore(wrapper, node);
|
|
74
118
|
wrapper.appendChild(node);
|
|
75
119
|
wrapped += 1;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -92,6 +92,23 @@ export interface ItemEntity extends VersionEntity, ConfigContainerEntity, Search
|
|
|
92
92
|
configuration?: any;
|
|
93
93
|
highlights?: Record<string, string[]>;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* `baseId` and `version` are optional on an entity a section references. They
|
|
97
|
+
* identify a stored version, which a host building a section from an item
|
|
98
|
+
* config does not have, and no player reads either.
|
|
99
|
+
*/
|
|
100
|
+
type WithOptionalVersion<T extends VersionEntity> = Omit<T, "baseId" | "version"> & Partial<Pick<VersionEntity, "baseId" | "version">>;
|
|
101
|
+
/**
|
|
102
|
+
* A passage as a section's item references it. `name` is optional on the same
|
|
103
|
+
* ground: the section player reads it only as a fallback for a missing `id`.
|
|
104
|
+
*/
|
|
105
|
+
export type ReferencedPassageEntity = Omit<WithOptionalVersion<PassageEntity>, "name"> & {
|
|
106
|
+
name?: string;
|
|
107
|
+
};
|
|
108
|
+
/** An item as `AssessmentItemRef.item` references it. */
|
|
109
|
+
export type ReferencedItemEntity = Omit<WithOptionalVersion<ItemEntity>, "passage"> & {
|
|
110
|
+
passage?: string | ReferencedPassageEntity | null;
|
|
111
|
+
};
|
|
95
112
|
/**
|
|
96
113
|
* Metadata specifically for interpretation by clients, typically containing
|
|
97
114
|
* options that are relevant for the user interface. This is not indexed for search.
|
|
@@ -146,7 +163,7 @@ export interface AssessmentItemRef extends SearchMetaDataEntity {
|
|
|
146
163
|
* Resolved item entity with PIE config.
|
|
147
164
|
* This is populated by the client before passing to the player.
|
|
148
165
|
*/
|
|
149
|
-
item?:
|
|
166
|
+
item?: ReferencedItemEntity;
|
|
150
167
|
/** Item-level settings for tool requirements and customization */
|
|
151
168
|
settings?: ItemSettings;
|
|
152
169
|
/**
|
|
@@ -182,14 +199,6 @@ export interface AssessmentSection extends SearchMetaDataEntity, SettingsMetaDat
|
|
|
182
199
|
*/
|
|
183
200
|
assessmentItemRefs?: AssessmentItemRef[];
|
|
184
201
|
rubricBlocks?: RubricBlock[];
|
|
185
|
-
/**
|
|
186
|
-
* QTI 3.0: Personal Needs Profile (PNP 3.0) for section-scoped delivery.
|
|
187
|
-
*
|
|
188
|
-
* Section players read this (falling back to `settings.personalNeedsProfile`,
|
|
189
|
-
* then to the computed default profile) to drive PNP policy when a section is
|
|
190
|
-
* delivered without an enclosing assessment.
|
|
191
|
-
*/
|
|
192
|
-
personalNeedsProfile?: PersonalNeedsProfile;
|
|
193
202
|
/**
|
|
194
203
|
* Formative delivery policy for this section: how many Tries a learner gets
|
|
195
204
|
* on each item, when feedback is revealed, and whether the behavior is on at
|
|
@@ -466,15 +475,7 @@ export interface AccessibilityCatalog {
|
|
|
466
475
|
export interface PersonalNeedsProfile {
|
|
467
476
|
supports: string[];
|
|
468
477
|
prohibitedSupports?: string[];
|
|
469
|
-
activateAtInit?: string[];
|
|
470
478
|
}
|
|
471
|
-
/**
|
|
472
|
-
* QTI 3.0: Reference to a shared stimulus (passage).
|
|
473
|
-
* Maps to qti-assessment-stimulus-ref element.
|
|
474
|
-
*
|
|
475
|
-
* NOTE: Not currently used. We embed PassageEntity directly in RubricBlock instead.
|
|
476
|
-
* Kept for potential future use if we need to support external stimulus references.
|
|
477
|
-
*/
|
|
478
479
|
export interface AssessmentEntity extends BaseEntity, SearchMetaDataEntity {
|
|
479
480
|
name?: string;
|
|
480
481
|
title?: string;
|
|
@@ -494,117 +495,41 @@ export interface AssessmentEntity extends BaseEntity, SearchMetaDataEntity {
|
|
|
494
495
|
* QTI 3.0: testParts structure (authoritative for QTI format).
|
|
495
496
|
*/
|
|
496
497
|
testParts?: TestPart[];
|
|
497
|
-
/** QTI 3.0: Stimulus material references (Phase 3 - placeholder) */
|
|
498
|
-
stimulusRefs?: any[];
|
|
499
498
|
}
|
|
500
499
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* tool configurations, and theme settings while remaining extensible.
|
|
500
|
+
* Assessment settings the tool policy reads: district policy, test
|
|
501
|
+
* administration and per-tool parameters.
|
|
504
502
|
*/
|
|
505
503
|
export interface AssessmentSettings {
|
|
506
504
|
/** District/organization policies */
|
|
507
505
|
districtPolicy?: {
|
|
508
506
|
blockedTools?: string[];
|
|
509
507
|
requiredTools?: string[];
|
|
510
|
-
policies?: Record<string, any>;
|
|
511
508
|
};
|
|
512
509
|
/** Test administration configuration */
|
|
513
510
|
testAdministration?: {
|
|
514
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Per-session override by tool id. `false` withdraws the tool and
|
|
513
|
+
* outranks every policy level except a district block. `true` grants it
|
|
514
|
+
* below a district block, an item's `restrictedTools` and the profile's
|
|
515
|
+
* `prohibitedSupports`, and above requirements and profile supports.
|
|
516
|
+
*/
|
|
515
517
|
toolOverrides?: Record<string, boolean>;
|
|
516
|
-
startDate?: string;
|
|
517
|
-
endDate?: string;
|
|
518
518
|
};
|
|
519
|
-
/**
|
|
519
|
+
/**
|
|
520
|
+
* Parameters by tool id, handed to a tool a grant or requirement admits;
|
|
521
|
+
* an item's `toolParameters` entry wins. Provider configuration belongs in
|
|
522
|
+
* the toolkit's `tools.providers`.
|
|
523
|
+
*/
|
|
520
524
|
toolConfigs?: {
|
|
521
525
|
calculator?: AssessmentCalculatorConfig;
|
|
522
|
-
|
|
523
|
-
* Text-to-speech configuration.
|
|
524
|
-
*
|
|
525
|
-
* Standard parameters (voice, rate, pitch) are portable across providers
|
|
526
|
-
* and follow W3C Web Speech API specifications.
|
|
527
|
-
*
|
|
528
|
-
* Provider-specific extensions should be placed in providerOptions.
|
|
529
|
-
*
|
|
530
|
-
* @see https://w3c.github.io/speech-api/
|
|
531
|
-
*/
|
|
532
|
-
textToSpeech?: {
|
|
533
|
-
/**
|
|
534
|
-
* TTS provider
|
|
535
|
-
*
|
|
536
|
-
* @standard "browser" uses W3C Web Speech API
|
|
537
|
-
* @extension "polly" and "custom" are provider-specific
|
|
538
|
-
*/
|
|
539
|
-
provider?: "browser" | "polly" | "custom";
|
|
540
|
-
/**
|
|
541
|
-
* Voice identifier (provider-specific names)
|
|
542
|
-
*
|
|
543
|
-
* @standard W3C Web Speech API (concept)
|
|
544
|
-
* @example "Joanna" (Polly), "en-US-Standard-A" (Google), browser voices
|
|
545
|
-
*/
|
|
546
|
-
voice?: string;
|
|
547
|
-
/**
|
|
548
|
-
* Speech rate (speed multiplier)
|
|
549
|
-
*
|
|
550
|
-
* @standard W3C Web Speech API
|
|
551
|
-
* @range 0.25 to 4.0
|
|
552
|
-
* @default 1.0
|
|
553
|
-
*/
|
|
554
|
-
rate?: number;
|
|
555
|
-
/**
|
|
556
|
-
* Pitch adjustment
|
|
557
|
-
*
|
|
558
|
-
* @standard W3C Web Speech API
|
|
559
|
-
* @range 0 to 2 (as multiplier)
|
|
560
|
-
* @default 1.0
|
|
561
|
-
*/
|
|
562
|
-
pitch?: number;
|
|
563
|
-
/**
|
|
564
|
-
* Speech Rule Engine options for generated MathML speech.
|
|
565
|
-
*
|
|
566
|
-
* `style` is passed to SRE directly; ClearSpeak combines multiple
|
|
567
|
-
* preferences with ":" (for example,
|
|
568
|
-
* "ImpliedTimes_MoreImpliedTimes:Paren_Silent").
|
|
569
|
-
*
|
|
570
|
-
* Mirrors assessment-toolkit's SREMathSpeechOptions without importing
|
|
571
|
-
* toolkit service types into the shared content contract package.
|
|
572
|
-
*/
|
|
573
|
-
mathSpeech?: {
|
|
574
|
-
domain?: string;
|
|
575
|
-
style?: string;
|
|
576
|
-
engineOptions?: Record<string, unknown>;
|
|
577
|
-
};
|
|
578
|
-
/**
|
|
579
|
-
* Provider-specific options (extensions)
|
|
580
|
-
*
|
|
581
|
-
* @extension Not portable across providers
|
|
582
|
-
* @example For AWS Polly: { engine: "neural", region: "us-east-1" }
|
|
583
|
-
* @example For Google Cloud: { audioEncoding: "MP3", effectsProfileId: ["headphone-class-device"] }
|
|
584
|
-
*/
|
|
585
|
-
providerOptions?: Record<string, any>;
|
|
586
|
-
};
|
|
587
|
-
[toolId: string]: any;
|
|
588
|
-
};
|
|
589
|
-
/** Theme configuration (not in PNP) */
|
|
590
|
-
themeConfig?: {
|
|
591
|
-
scheme?: "default" | "high-contrast" | "dark";
|
|
592
|
-
fontSize?: number;
|
|
593
|
-
fontFamily?: string;
|
|
594
|
-
lineHeight?: number;
|
|
595
|
-
reducedMotion?: boolean;
|
|
526
|
+
[toolId: string]: Record<string, unknown> | undefined;
|
|
596
527
|
};
|
|
597
|
-
/** Product-specific extensions */
|
|
598
|
-
[key: string]: any;
|
|
599
528
|
}
|
|
600
|
-
/**
|
|
601
|
-
|
|
602
|
-
* The engine is implicit (Desmos).
|
|
603
|
-
*/
|
|
604
|
-
export interface AssessmentCalculatorConfig {
|
|
529
|
+
/** Calculator parameters in assessment settings. */
|
|
530
|
+
export type AssessmentCalculatorConfig = {
|
|
605
531
|
type?: "basic" | "scientific" | "graphing";
|
|
606
|
-
|
|
607
|
-
}
|
|
532
|
+
};
|
|
608
533
|
/**
|
|
609
534
|
* Item-level settings for tool requirements.
|
|
610
535
|
* Used in AssessmentItemRef.settings to specify item-specific tool needs.
|
|
@@ -612,8 +537,8 @@ export interface AssessmentCalculatorConfig {
|
|
|
612
537
|
export interface ItemSettings {
|
|
613
538
|
requiredTools?: string[];
|
|
614
539
|
restrictedTools?: string[];
|
|
615
|
-
|
|
616
|
-
|
|
540
|
+
/** Parameters by tool id; outranks the assessment's `toolConfigs` entry. */
|
|
541
|
+
toolParameters?: Record<string, Record<string, unknown>>;
|
|
617
542
|
}
|
|
618
543
|
export type PlayerMode = "gather" | "view" | "evaluate" | "author";
|
|
619
544
|
export type PlayerRole = "student" | "instructor";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-players-shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.75",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared runtime + UI utilities for PIE players",
|
|
6
6
|
"license": "MIT",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"dist"
|
|
103
103
|
],
|
|
104
104
|
"dependencies": {
|
|
105
|
+
"@pie-element/shared-utils": "0.1.3",
|
|
105
106
|
"@pie-lib/math-rendering-module": "6.0.0",
|
|
106
107
|
"dompurify": "^3.4.16",
|
|
107
108
|
"semver": "^7.8.5"
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
"scripts": {
|
|
110
111
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && node scripts/bundle-vendored-modules.mjs && mkdir -p dist/components && cp -R src/components/vendor dist/components/vendor",
|
|
111
112
|
"dev": "tsc -p tsconfig.json --watch",
|
|
112
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
113
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json",
|
|
113
114
|
"lint": "biome lint .",
|
|
114
115
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
115
116
|
"test": "bun test",
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
"devDependencies": {
|
|
120
121
|
"@biomejs/biome": "^2.5.15",
|
|
121
122
|
"@happy-dom/global-registrator": "^20.14.5",
|
|
122
|
-
"@pie-element/shared-math-rendering-mathjax": "0.1.
|
|
123
|
+
"@pie-element/shared-math-rendering-mathjax": "0.1.4",
|
|
123
124
|
"@playwright/test": "^1.63.0",
|
|
124
125
|
"@types/semver": "^7.8.0",
|
|
125
126
|
"es-module-shims": "^2.8.4",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Forwards the MathJax version-conflict event to instrumentation.
|
|
3
|
-
*
|
|
4
|
-
* The MathJax 4 adapter that browser ESM elements render with dispatches
|
|
5
|
-
* `pie-mathjax-version-conflict` on `window`, once per condition per page, when
|
|
6
|
-
* MathJax 3 runs on the same page — an unsupported page, see
|
|
7
|
-
* docs/item-player/loading-strategies.md#one-mathjax-version-per-page. The
|
|
8
|
-
* event name is repeated here because players do not depend on the element
|
|
9
|
-
* packages.
|
|
10
|
-
*/
|
|
11
|
-
import type { InstrumentationProvider } from "../instrumentation/types.js";
|
|
12
|
-
export declare const MATHJAX_VERSION_CONFLICT_EVENT = "pie-mathjax-version-conflict";
|
|
13
|
-
/**
|
|
14
|
-
* Tracks each `pie-mathjax-version-conflict` event on `target` with the
|
|
15
|
-
* provider `getProvider` returns when it fires, once per provider.
|
|
16
|
-
*/
|
|
17
|
-
export declare function forwardMathjaxVersionConflicts(target: EventTarget, getProvider: () => InstrumentationProvider | undefined): void;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Forwards the MathJax version-conflict event to instrumentation.
|
|
3
|
-
*
|
|
4
|
-
* The MathJax 4 adapter that browser ESM elements render with dispatches
|
|
5
|
-
* `pie-mathjax-version-conflict` on `window`, once per condition per page, when
|
|
6
|
-
* MathJax 3 runs on the same page — an unsupported page, see
|
|
7
|
-
* docs/item-player/loading-strategies.md#one-mathjax-version-per-page. The
|
|
8
|
-
* event name is repeated here because players do not depend on the element
|
|
9
|
-
* packages.
|
|
10
|
-
*/
|
|
11
|
-
export const MATHJAX_VERSION_CONFLICT_EVENT = "pie-mathjax-version-conflict";
|
|
12
|
-
// Every ESM backend on a page listens, and each player bundle carries its own
|
|
13
|
-
// copy of this module, so the providers that tracked an event are kept on the
|
|
14
|
-
// page.
|
|
15
|
-
const TRACKED = Symbol.for("@pie-players/pie-players-shared/mathjax-version-conflict-tracked");
|
|
16
|
-
function providersThatTracked(event) {
|
|
17
|
-
const registry = globalThis;
|
|
18
|
-
registry[TRACKED] ??= new WeakMap();
|
|
19
|
-
let providers = registry[TRACKED].get(event);
|
|
20
|
-
if (!providers) {
|
|
21
|
-
providers = new WeakSet();
|
|
22
|
-
registry[TRACKED].set(event, providers);
|
|
23
|
-
}
|
|
24
|
-
return providers;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Tracks each `pie-mathjax-version-conflict` event on `target` with the
|
|
28
|
-
* provider `getProvider` returns when it fires, once per provider.
|
|
29
|
-
*/
|
|
30
|
-
export function forwardMathjaxVersionConflicts(target, getProvider) {
|
|
31
|
-
target.addEventListener(MATHJAX_VERSION_CONFLICT_EVENT, (event) => {
|
|
32
|
-
const provider = getProvider();
|
|
33
|
-
if (!provider)
|
|
34
|
-
return;
|
|
35
|
-
const tracked = providersThatTracked(event);
|
|
36
|
-
if (tracked.has(provider))
|
|
37
|
-
return;
|
|
38
|
-
tracked.add(provider);
|
|
39
|
-
const detail = event.detail;
|
|
40
|
-
const { condition, docsUrl } = (detail && typeof detail === "object" ? detail : {});
|
|
41
|
-
try {
|
|
42
|
-
provider.trackEvent(MATHJAX_VERSION_CONFLICT_EVENT, {
|
|
43
|
-
condition,
|
|
44
|
-
docsUrl,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Swallow: instrumentation must never break rendering.
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { type Stage, type StageChangeDetail, type StageSourceCe, type StageStatus } from "./stages.js";
|
|
2
|
-
/**
|
|
3
|
-
* Stage tracker primitive (M6).
|
|
4
|
-
*
|
|
5
|
-
* One tracker per CE instance per cohort. `enter(stage, status?)` is the
|
|
6
|
-
* only mutation; `reset()` rolls the cohort over (e.g. when
|
|
7
|
-
* `(sectionId, attemptId)` changes).
|
|
8
|
-
*
|
|
9
|
-
* The tracker enforces the locked invariants from
|
|
10
|
-
* `m6_ready_vocab_canonical_followup.plan.md` §3.1–§3.6:
|
|
11
|
-
* - Monotonic ordering across `STAGES`. Backward transitions inside one
|
|
12
|
-
* cohort are rejected (dev) / warned-once (prod) so subscribers never
|
|
13
|
-
* see a stage regress without an intervening `reset()`.
|
|
14
|
-
* - Per-CE applicability. The post-retro canonical list applies to
|
|
15
|
-
* every CE shape; the `applicableStages` indirection is kept so a
|
|
16
|
-
* future shape-specific stage can opt out without rewriting callers.
|
|
17
|
-
* Non-applicable stages auto-emit `status: "skipped"` when an
|
|
18
|
-
* applicable later stage is entered, so iteration order stays stable
|
|
19
|
-
* across CE shapes.
|
|
20
|
-
* - One source for every emit. The injected `emit(detail)` function
|
|
21
|
-
* drives DOM events, engine subscribers, and coordinator subscribers
|
|
22
|
-
* from the same call site so no surface drifts.
|
|
23
|
-
*
|
|
24
|
-
* Failure semantics intentionally piggyback on the M3 framework-error
|
|
25
|
-
* contract: `enter(stage, "failed")` records the position; the
|
|
26
|
-
* framework error records the cause.
|
|
27
|
-
*/
|
|
28
|
-
export type CreateStageTrackerOptions = {
|
|
29
|
-
sourceCe: string;
|
|
30
|
-
sourceCeShape: StageSourceCe;
|
|
31
|
-
runtimeId: string;
|
|
32
|
-
sectionId?: string;
|
|
33
|
-
attemptId?: string;
|
|
34
|
-
emit: (detail: StageChangeDetail) => void;
|
|
35
|
-
now?: () => string;
|
|
36
|
-
onUnexpectedTransition?: (info: {
|
|
37
|
-
from: Stage | null;
|
|
38
|
-
to: Stage;
|
|
39
|
-
reason: "backward" | "duplicate";
|
|
40
|
-
}) => void;
|
|
41
|
-
};
|
|
42
|
-
export type StageTracker = {
|
|
43
|
-
enter: (stage: Stage, status?: StageStatus) => void;
|
|
44
|
-
reset: (next?: {
|
|
45
|
-
sectionId?: string;
|
|
46
|
-
attemptId?: string;
|
|
47
|
-
}) => void;
|
|
48
|
-
getCurrent: () => Stage | null;
|
|
49
|
-
};
|
|
50
|
-
export declare function createStageTracker(opts: CreateStageTrackerOptions): StageTracker;
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { STAGES, applicableStages, stageOrdinal, } from "./stages.js";
|
|
2
|
-
let unexpectedTransitionWarned = false;
|
|
3
|
-
function defaultUnexpectedTransitionLogger(info) {
|
|
4
|
-
if (unexpectedTransitionWarned)
|
|
5
|
-
return;
|
|
6
|
-
unexpectedTransitionWarned = true;
|
|
7
|
-
if (typeof console !== "undefined" && console.warn) {
|
|
8
|
-
console.warn(`[pie-stage-tracker] Unexpected ${info.reason} stage transition ` +
|
|
9
|
-
`from \`${info.from ?? "<none>"}\` to \`${info.to}\`. ` +
|
|
10
|
-
`Stage transitions are expected to be monotonic within a cohort; ` +
|
|
11
|
-
`call \`reset()\` when \`(sectionId, attemptId)\` changes.`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function createStageTracker(opts) {
|
|
15
|
-
const applicable = applicableStages(opts.sourceCeShape);
|
|
16
|
-
const applicableSet = new Set(applicable);
|
|
17
|
-
const now = opts.now ?? (() => new Date().toISOString());
|
|
18
|
-
const onUnexpected = opts.onUnexpectedTransition ?? defaultUnexpectedTransitionLogger;
|
|
19
|
-
let current = null;
|
|
20
|
-
let sectionId = opts.sectionId;
|
|
21
|
-
let attemptId = opts.attemptId;
|
|
22
|
-
const emitted = new Set();
|
|
23
|
-
function emitDetail(stage, status) {
|
|
24
|
-
opts.emit({
|
|
25
|
-
stage,
|
|
26
|
-
status,
|
|
27
|
-
runtimeId: opts.runtimeId,
|
|
28
|
-
sectionId,
|
|
29
|
-
attemptId,
|
|
30
|
-
timestamp: now(),
|
|
31
|
-
sourceCe: opts.sourceCe,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
function fillSkippedBefore(stage) {
|
|
35
|
-
const targetIdx = stageOrdinal(stage);
|
|
36
|
-
const startIdx = current === null ? 0 : stageOrdinal(current) + 1;
|
|
37
|
-
for (let idx = startIdx; idx < targetIdx; idx += 1) {
|
|
38
|
-
const skipCandidate = STAGES[idx];
|
|
39
|
-
if (applicableSet.has(skipCandidate))
|
|
40
|
-
continue;
|
|
41
|
-
if (emitted.has(skipCandidate))
|
|
42
|
-
continue;
|
|
43
|
-
emitted.add(skipCandidate);
|
|
44
|
-
emitDetail(skipCandidate, "skipped");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function enter(stage, status = "entered") {
|
|
48
|
-
const nextOrdinal = stageOrdinal(stage);
|
|
49
|
-
const currentOrdinal = current === null ? -1 : stageOrdinal(current);
|
|
50
|
-
if (status === "entered") {
|
|
51
|
-
if (emitted.has(stage)) {
|
|
52
|
-
onUnexpected({ from: current, to: stage, reason: "duplicate" });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (nextOrdinal < currentOrdinal) {
|
|
56
|
-
onUnexpected({ from: current, to: stage, reason: "backward" });
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (!applicableSet.has(stage)) {
|
|
60
|
-
// A non-applicable stage entered with `entered` is a programming
|
|
61
|
-
// error: the tracker auto-skips it when a later stage enters.
|
|
62
|
-
// Treat it as a `skipped` emission and continue.
|
|
63
|
-
if (emitted.has(stage))
|
|
64
|
-
return;
|
|
65
|
-
emitted.add(stage);
|
|
66
|
-
emitDetail(stage, "skipped");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
fillSkippedBefore(stage);
|
|
70
|
-
emitted.add(stage);
|
|
71
|
-
current = stage;
|
|
72
|
-
emitDetail(stage, "entered");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// `status === "failed"` records position; do not gate on monotonic
|
|
76
|
-
// ordering — failure can happen during any transition. Mark the stage
|
|
77
|
-
// as emitted so a subsequent `entered` for the same stage warns as a
|
|
78
|
-
// duplicate; downstream recovery should call `reset()` first.
|
|
79
|
-
if (status === "failed") {
|
|
80
|
-
fillSkippedBefore(stage);
|
|
81
|
-
emitted.add(stage);
|
|
82
|
-
current = stage;
|
|
83
|
-
emitDetail(stage, "failed");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// `status === "skipped"` is reserved for the auto-fill path; explicit
|
|
87
|
-
// callers shouldn't pass it. Accept it idempotently for robustness.
|
|
88
|
-
if (emitted.has(stage))
|
|
89
|
-
return;
|
|
90
|
-
emitted.add(stage);
|
|
91
|
-
emitDetail(stage, "skipped");
|
|
92
|
-
}
|
|
93
|
-
function reset(next) {
|
|
94
|
-
current = null;
|
|
95
|
-
emitted.clear();
|
|
96
|
-
if (next) {
|
|
97
|
-
sectionId = next.sectionId;
|
|
98
|
-
attemptId = next.attemptId;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function getCurrent() {
|
|
102
|
-
return current;
|
|
103
|
-
}
|
|
104
|
-
return { enter, reset, getCurrent };
|
|
105
|
-
}
|