@pie-players/pie-players-shared 1.0.0-beta.8 → 1.0.0-beta.9
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 +13 -13
- package/dist/i18n/messages/en-US.d.ts +4 -4
- package/dist/i18n/messages/en-US.js +4 -4
- package/dist/i18n/types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/item-session-control/index.d.ts +16 -0
- package/dist/item-session-control/index.js +5 -0
- package/dist/{formative → item-session-control}/mastery.d.ts +5 -5
- package/dist/{formative → item-session-control}/mastery.js +3 -3
- package/dist/{formative → item-session-control}/outcome.d.ts +3 -3
- package/dist/{formative → item-session-control}/outcome.js +2 -2
- package/dist/{formative → item-session-control}/policy.d.ts +11 -11
- package/dist/{formative → item-session-control}/policy.js +12 -12
- package/dist/item-session-control/session.d.ts +19 -0
- package/dist/{formative → item-session-control}/session.js +12 -12
- package/dist/{formative → item-session-control}/state.d.ts +26 -26
- package/dist/{formative → item-session-control}/state.js +13 -13
- package/dist/{formative → item-session-control}/types.d.ts +38 -40
- package/dist/{formative → item-session-control}/types.js +2 -2
- package/dist/timed-media/index.d.ts +1 -1
- package/dist/timed-media/index.js +1 -1
- package/dist/timed-media/policy.d.ts +2 -2
- package/dist/timed-media/policy.js +2 -2
- package/dist/timed-media/session.d.ts +1 -1
- package/dist/timed-media/session.js +1 -1
- package/dist/timed-media/types.d.ts +11 -11
- package/dist/types/index.d.ts +16 -16
- package/package.json +4 -4
- package/dist/formative/index.d.ts +0 -16
- package/dist/formative/index.js +0 -5
- package/dist/formative/session.d.ts +0 -19
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shared runtime utilities for the `@pie-players/*` player suite: the element
|
|
4
4
|
loader, PIE config and session helpers, markup security, i18n, and the
|
|
5
|
-
|
|
5
|
+
item session control, timed-media and media contracts. For framework code and host
|
|
6
6
|
integrations that need the helpers the player packages use.
|
|
7
7
|
|
|
8
8
|
## Public Exports
|
|
@@ -23,7 +23,7 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
23
23
|
| `/security/private-hosts` | `isPrivateHostname` and `isCloudMetadataHostname`, the host classification for a URL a server fetches on someone else's behalf. Node-safe: it loads none of the markup sanitizers |
|
|
24
24
|
| `/object` | `mergeObjectsIgnoringNullUndefined`, `cloneDeep`, `isPlainRecord` |
|
|
25
25
|
| `/types` | The shared types: item, passage, section and session entities, PIE models and controllers, accessibility catalogs, and the [media vocabulary](#media-validation) |
|
|
26
|
-
| `/
|
|
26
|
+
| `/item-session-control` | [Item session control](#item-session-control) |
|
|
27
27
|
| `/timed-media` | [Timed media](#timed-media) |
|
|
28
28
|
| `/media` | [Media validation](#media-validation) |
|
|
29
29
|
| `/pie` | PIE runtime utilities: config helpers, bundle initialization, `ItemController` and the item session contract, session commit and snapshots, authoring helpers, math renderer setup, instrumentation event maps ([PIE utilities](src/pie/README.md)) |
|
|
@@ -40,9 +40,9 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
40
40
|
| `/ui/fontawesome` | The FontAwesome `<nds-icon-button>` glyphs need: the one FA Free pin, Pro detection, the `<head>` and shadow-root installs, and the glyph weight. The assessment toolkit, the section player and `@pie-players/pie-tool-tts-inline` use it |
|
|
41
41
|
| `/tools/term-lookup` | Term normalization and the lookup session the dictionary tools share; the host supplies the endpoint or a resolver |
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Item Session Control
|
|
44
44
|
|
|
45
|
-
`@pie-players/pie-players-shared/
|
|
45
|
+
`@pie-players/pie-players-shared/item-session-control` holds the policy resolution, outcome
|
|
46
46
|
aggregation, Try-state reducer, item view, and mastery rollup that
|
|
47
47
|
`pie-section-player` drives. It is pure (no DOM, no timers, no element
|
|
48
48
|
registry), so it is testable without a browser and importable by an adapter that
|
|
@@ -50,18 +50,18 @@ has no player.
|
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} from "@pie-players/pie-players-shared/
|
|
53
|
+
aggregateTryOutcome,
|
|
54
|
+
resolveItemSessionControl,
|
|
55
|
+
resolveItemTryView,
|
|
56
|
+
rollupMastery,
|
|
57
|
+
} from "@pie-players/pie-players-shared/item-session-control";
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The authored half, `
|
|
61
|
-
`
|
|
60
|
+
The authored half, `ItemSessionControlPolicy` on `AssessmentSection` and
|
|
61
|
+
`AssessmentItemRef`, is re-exported from the package
|
|
62
62
|
root and from `/types`, beside the section types it annotates.
|
|
63
63
|
|
|
64
|
-
The contract is [`docs/prds/
|
|
64
|
+
The contract is [`docs/prds/item-session-control-contract.md`](../../docs/prds/item-session-control-contract.md),
|
|
65
65
|
including the QTI 3 mapping and why PIE says **Try** rather than "attempt".
|
|
66
66
|
|
|
67
67
|
## Timed Media
|
|
@@ -94,7 +94,7 @@ re-exported from the package root and from `/types`, beside the section types it
|
|
|
94
94
|
annotates.
|
|
95
95
|
|
|
96
96
|
The contract is [`docs/prds/timed-media-section-contract.md`](../../docs/prds/timed-media-section-contract.md),
|
|
97
|
-
including why cue gate conditions name the
|
|
97
|
+
including why cue gate conditions name the Try vocabulary and why an
|
|
98
98
|
unenforceable policy degrades to advisory rather than failing closed.
|
|
99
99
|
|
|
100
100
|
## Media Validation
|
|
@@ -109,16 +109,16 @@ declare const enUS: {
|
|
|
109
109
|
loadFailed: string;
|
|
110
110
|
restoreFailed: string;
|
|
111
111
|
};
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
/** Item session control: check-answer control and its outcome announcements. */
|
|
113
|
+
check: {
|
|
114
|
+
answer: string;
|
|
115
115
|
tryAgain: string;
|
|
116
116
|
answerRecorded: string;
|
|
117
117
|
correct: string;
|
|
118
118
|
partlyCorrect: string;
|
|
119
119
|
notCorrect: string;
|
|
120
120
|
notAutoScored: string;
|
|
121
|
-
|
|
121
|
+
failed: string;
|
|
122
122
|
/**
|
|
123
123
|
* Appended after the outcome, so the announcement reads
|
|
124
124
|
* "Not correct. 2 tries left." Kept a separate plural group rather than
|
|
@@ -109,16 +109,16 @@ const enUS = {
|
|
|
109
109
|
loadFailed: "The assessment could not be loaded. Try again.",
|
|
110
110
|
restoreFailed: "Your saved answers for this section could not be restored. Try again.",
|
|
111
111
|
},
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
/** Item session control: check-answer control and its outcome announcements. */
|
|
113
|
+
check: {
|
|
114
|
+
answer: "Check answer",
|
|
115
115
|
tryAgain: "Try again",
|
|
116
116
|
answerRecorded: "Answer recorded.",
|
|
117
117
|
correct: "Correct.",
|
|
118
118
|
partlyCorrect: "Partly correct.",
|
|
119
119
|
notCorrect: "Not correct.",
|
|
120
120
|
notAutoScored: "Answer recorded. This question is not scored automatically.",
|
|
121
|
-
|
|
121
|
+
failed: "This question could not be checked. Try again.",
|
|
122
122
|
/**
|
|
123
123
|
* Appended after the outcome, so the announcement reads
|
|
124
124
|
* "Not correct. 2 tries left." Kept a separate plural group rather than
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ type PluralCategoryKey = keyof PluralTranslation;
|
|
|
57
57
|
* Dot-notation keys of a catalog shape.
|
|
58
58
|
*
|
|
59
59
|
* A plural group terminates the path at the group itself: `plural()` is called
|
|
60
|
-
* with `player.
|
|
60
|
+
* with `player.check.triesLeft`, never with `…triesLeft.other`. A group is
|
|
61
61
|
* recognized by every one of its keys being a CLDR plural category — the
|
|
62
62
|
* containment has to run this way round, because a group carrying only
|
|
63
63
|
* `one`/`other` does not contain the whole category union.
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * from "./security/index.js";
|
|
|
6
6
|
export * from "./loaders/index.js";
|
|
7
7
|
export * from "./object/index.js";
|
|
8
8
|
export * from "./pie/index.js";
|
|
9
|
-
export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env,
|
|
9
|
+
export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env, ItemSessionControlPolicy, TimedMediaSectionData, ItemConfig, ItemEntity, ItemSession, OutcomeResponse, PassageEntity, PieController, PieContent, PieDefaultModel, PieItemElement, PieModel, PieUpdateSession, ReferencedItemEntity, ReferencedPassageEntity, RubricBlock, TestPart, } from "./types/index.js";
|
|
10
10
|
export { editorPostFix } from "./types/index.js";
|
|
11
11
|
export * from "./ui/attribute-coercion.js";
|
|
12
12
|
export * from "./ui/content-styles.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item session control: Try state, feedback reveal, and section mastery.
|
|
3
|
+
*
|
|
4
|
+
* Pure and Node-safe by design — no DOM, no timers, no element registry — so
|
|
5
|
+
* the contract is testable without a browser and an adapter can import it
|
|
6
|
+
* without pulling in a player. Everything that touches the DOM stays in
|
|
7
|
+
* `@pie-players/pie-section-player`.
|
|
8
|
+
*
|
|
9
|
+
* Contract: `docs/prds/item-session-control-contract.md`.
|
|
10
|
+
*/
|
|
11
|
+
export type { TryAction, TryCorrectness, ItemSessionControlPolicy, FeedbackEnvOverride, FeedbackReveal, ItemTryState, ItemTryView, MasteryRollup, FeedbackRevealTiming, ScoredElementOutcome, TrySectionProjection, TrySectionSlice, TryLimit, TryOutcome, ResolvedItemSessionControlPolicy, } from "./types.js";
|
|
12
|
+
export { ITEM_SESSION_CONTROL_DEFAULTS, isItemSessionControlEnabledInSection, resolveItemSessionControls, resolveItemSessionControl, } from "./policy.js";
|
|
13
|
+
export { aggregateTryOutcome } from "./outcome.js";
|
|
14
|
+
export { ITEM_TRY_STATE_VERSION, createItemTryState, hideItemFeedback, recordTry, resolveItemTryView, retryItem, revealItemFeedback, } from "./state.js";
|
|
15
|
+
export { MASTERY_ROLLUP_VERSION, rollupMastery } from "./mastery.js";
|
|
16
|
+
export { TRY_SLICE_VERSION, normalizeTrySectionSlice, toTrySectionSlice, } from "./session.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ITEM_SESSION_CONTROL_DEFAULTS, isItemSessionControlEnabledInSection, resolveItemSessionControls, resolveItemSessionControl, } from "./policy.js";
|
|
2
|
+
export { aggregateTryOutcome } from "./outcome.js";
|
|
3
|
+
export { ITEM_TRY_STATE_VERSION, createItemTryState, hideItemFeedback, recordTry, resolveItemTryView, retryItem, revealItemFeedback, } from "./state.js";
|
|
4
|
+
export { MASTERY_ROLLUP_VERSION, rollupMastery } from "./mastery.js";
|
|
5
|
+
export { TRY_SLICE_VERSION, normalizeTrySectionSlice, toTrySectionSlice, } from "./session.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
1
|
+
import type { ItemTryState, MasteryRollup } from "./types.js";
|
|
2
|
+
export declare const MASTERY_ROLLUP_VERSION: 1;
|
|
3
3
|
/**
|
|
4
4
|
* Roll Try outcomes up to the section.
|
|
5
5
|
*
|
|
@@ -10,7 +10,7 @@ export declare const FORMATIVE_MASTERY_VERSION: 1;
|
|
|
10
10
|
* excluded: nothing yet says it cannot be scored, so it keeps the section from
|
|
11
11
|
* reading as complete after one correct answer.
|
|
12
12
|
*/
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function rollupMastery(args: {
|
|
14
14
|
itemIdentifiers: readonly string[];
|
|
15
|
-
states: Record<string,
|
|
16
|
-
}):
|
|
15
|
+
states: Record<string, ItemTryState>;
|
|
16
|
+
}): MasteryRollup;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const MASTERY_ROLLUP_VERSION = 1;
|
|
2
2
|
/**
|
|
3
3
|
* Roll Try outcomes up to the section.
|
|
4
4
|
*
|
|
@@ -9,7 +9,7 @@ export const FORMATIVE_MASTERY_VERSION = 1;
|
|
|
9
9
|
* excluded: nothing yet says it cannot be scored, so it keeps the section from
|
|
10
10
|
* reading as complete after one correct answer.
|
|
11
11
|
*/
|
|
12
|
-
export function
|
|
12
|
+
export function rollupMastery(args) {
|
|
13
13
|
const identifiers = args.itemIdentifiers ?? [];
|
|
14
14
|
const totalItems = identifiers.length;
|
|
15
15
|
let notScorableItems = 0;
|
|
@@ -31,7 +31,7 @@ export function rollupFormativeMastery(args) {
|
|
|
31
31
|
}
|
|
32
32
|
const scorableItems = Math.max(0, totalItems - notScorableItems);
|
|
33
33
|
return {
|
|
34
|
-
version:
|
|
34
|
+
version: MASTERY_ROLLUP_VERSION,
|
|
35
35
|
totalItems,
|
|
36
36
|
scorableItems,
|
|
37
37
|
masteredItems,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScoredElementOutcome, TryOutcome } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Reduce the array `pie-item-player.provideScore()` returns to one Try outcome.
|
|
4
4
|
*
|
|
5
5
|
* The aggregation mirrors the policy the persisted API scoring path already
|
|
6
6
|
* documents (`docs/item-player/scoring-and-rubrics.md`), so a browser-derived
|
|
7
|
-
*
|
|
7
|
+
* Try result and a server-derived score do not disagree about what a
|
|
8
8
|
* multi-element item is worth:
|
|
9
9
|
*
|
|
10
10
|
* - one scored outcome: `points = score`, `max = max ?? 1`;
|
|
@@ -16,4 +16,4 @@ import type { FormativeScoredOutcome, FormativeTryOutcome } from "./types.js";
|
|
|
16
16
|
* those slots count toward `totalElementCount` and not toward
|
|
17
17
|
* `scoredElementCount`.
|
|
18
18
|
*/
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function aggregateTryOutcome(outcomes: ReadonlyArray<ScoredElementOutcome | null | undefined> | null): TryOutcome;
|
|
@@ -39,7 +39,7 @@ function classify(points, max) {
|
|
|
39
39
|
*
|
|
40
40
|
* The aggregation mirrors the policy the persisted API scoring path already
|
|
41
41
|
* documents (`docs/item-player/scoring-and-rubrics.md`), so a browser-derived
|
|
42
|
-
*
|
|
42
|
+
* Try result and a server-derived score do not disagree about what a
|
|
43
43
|
* multi-element item is worth:
|
|
44
44
|
*
|
|
45
45
|
* - one scored outcome: `points = score`, `max = max ?? 1`;
|
|
@@ -51,7 +51,7 @@ function classify(points, max) {
|
|
|
51
51
|
* those slots count toward `totalElementCount` and not toward
|
|
52
52
|
* `scoredElementCount`.
|
|
53
53
|
*/
|
|
54
|
-
export function
|
|
54
|
+
export function aggregateTryOutcome(outcomes) {
|
|
55
55
|
const all = Array.isArray(outcomes) ? outcomes : [];
|
|
56
56
|
const scored = all.filter(isScored);
|
|
57
57
|
const totalElementCount = all.length;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ItemSessionControlPolicy, ResolvedItemSessionControlPolicy } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Built-in defaults. `enabled: false` is what makes this contract invisible to
|
|
4
|
-
* every section authored before it: an absent `
|
|
4
|
+
* every section authored before it: an absent `itemSessionControl` field resolves to a
|
|
5
5
|
* disabled policy, and a disabled policy produces no state, no projection and
|
|
6
6
|
* no control.
|
|
7
7
|
*
|
|
8
8
|
* `maxTries: 1` matches QTI 3's `qti-item-session-control@max-attempts` default.
|
|
9
9
|
*/
|
|
10
|
-
export declare const
|
|
10
|
+
export declare const ITEM_SESSION_CONTROL_DEFAULTS: ResolvedItemSessionControlPolicy;
|
|
11
11
|
/**
|
|
12
12
|
* Merge built-in defaults, the section policy, and one item ref's override, in
|
|
13
13
|
* that order. Each field overrides independently, which is the order QTI 3 uses
|
|
@@ -17,21 +17,21 @@ export declare const FORMATIVE_POLICY_DEFAULTS: ResolvedFormativePolicy;
|
|
|
17
17
|
* section: authored policy is host-supplied data, and a typo in one field should
|
|
18
18
|
* not cost the learner the whole item.
|
|
19
19
|
*/
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function resolveItemSessionControl(sectionPolicy?: ItemSessionControlPolicy | null, itemPolicy?: ItemSessionControlPolicy | null): ResolvedItemSessionControlPolicy;
|
|
21
21
|
/**
|
|
22
22
|
* Resolve one policy per item in section order.
|
|
23
23
|
*
|
|
24
24
|
* Keyed by the identifier the caller supplies, which in the section runtime is
|
|
25
25
|
* the canonical item id — the same key `itemSessions` and the completion map
|
|
26
|
-
* use, so
|
|
26
|
+
* use, so Try state and an item session are always addressable by one
|
|
27
27
|
* id.
|
|
28
28
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
sectionPolicy?:
|
|
29
|
+
export declare function resolveItemSessionControls(args: {
|
|
30
|
+
sectionPolicy?: ItemSessionControlPolicy | null;
|
|
31
31
|
items: ReadonlyArray<{
|
|
32
32
|
identifier: string;
|
|
33
|
-
policy?:
|
|
33
|
+
policy?: ItemSessionControlPolicy | null;
|
|
34
34
|
}>;
|
|
35
|
-
}): Record<string,
|
|
36
|
-
/** True when at least one item in the section
|
|
37
|
-
export declare function
|
|
35
|
+
}): Record<string, ResolvedItemSessionControlPolicy>;
|
|
36
|
+
/** True when at least one item in the section enables item session control. */
|
|
37
|
+
export declare function isItemSessionControlEnabledInSection(policies: Record<string, ResolvedItemSessionControlPolicy>): boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in defaults. `enabled: false` is what makes this contract invisible to
|
|
3
|
-
* every section authored before it: an absent `
|
|
3
|
+
* every section authored before it: an absent `itemSessionControl` field resolves to a
|
|
4
4
|
* disabled policy, and a disabled policy produces no state, no projection and
|
|
5
5
|
* no control.
|
|
6
6
|
*
|
|
7
7
|
* `maxTries: 1` matches QTI 3's `qti-item-session-control@max-attempts` default.
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
9
|
+
export const ITEM_SESSION_CONTROL_DEFAULTS = {
|
|
10
10
|
enabled: false,
|
|
11
11
|
maxTries: 1,
|
|
12
12
|
feedback: "correctness",
|
|
@@ -40,21 +40,21 @@ function normalizeRevealTiming(value) {
|
|
|
40
40
|
* section: authored policy is host-supplied data, and a typo in one field should
|
|
41
41
|
* not cost the learner the whole item.
|
|
42
42
|
*/
|
|
43
|
-
export function
|
|
43
|
+
export function resolveItemSessionControl(sectionPolicy, itemPolicy) {
|
|
44
44
|
const enabled = typeof itemPolicy?.enabled === "boolean"
|
|
45
45
|
? itemPolicy.enabled
|
|
46
46
|
: typeof sectionPolicy?.enabled === "boolean"
|
|
47
47
|
? sectionPolicy.enabled
|
|
48
|
-
:
|
|
48
|
+
: ITEM_SESSION_CONTROL_DEFAULTS.enabled;
|
|
49
49
|
const maxTries = normalizeTryLimit(itemPolicy?.maxTries) ??
|
|
50
50
|
normalizeTryLimit(sectionPolicy?.maxTries) ??
|
|
51
|
-
|
|
51
|
+
ITEM_SESSION_CONTROL_DEFAULTS.maxTries;
|
|
52
52
|
const feedback = normalizeFeedback(itemPolicy?.feedback) ??
|
|
53
53
|
normalizeFeedback(sectionPolicy?.feedback) ??
|
|
54
|
-
|
|
54
|
+
ITEM_SESSION_CONTROL_DEFAULTS.feedback;
|
|
55
55
|
const requestedRevealOn = normalizeRevealTiming(itemPolicy?.revealOn) ??
|
|
56
56
|
normalizeRevealTiming(sectionPolicy?.revealOn) ??
|
|
57
|
-
|
|
57
|
+
ITEM_SESSION_CONTROL_DEFAULTS.revealOn;
|
|
58
58
|
// `"on-final-try"` has no referent under unlimited Tries — there is no final
|
|
59
59
|
// Try to reveal on — so it resolves to the only reading that keeps feedback
|
|
60
60
|
// reachable.
|
|
@@ -68,19 +68,19 @@ export function resolveFormativePolicy(sectionPolicy, itemPolicy) {
|
|
|
68
68
|
*
|
|
69
69
|
* Keyed by the identifier the caller supplies, which in the section runtime is
|
|
70
70
|
* the canonical item id — the same key `itemSessions` and the completion map
|
|
71
|
-
* use, so
|
|
71
|
+
* use, so Try state and an item session are always addressable by one
|
|
72
72
|
* id.
|
|
73
73
|
*/
|
|
74
|
-
export function
|
|
74
|
+
export function resolveItemSessionControls(args) {
|
|
75
75
|
const resolved = {};
|
|
76
76
|
for (const item of args.items) {
|
|
77
77
|
if (!item?.identifier)
|
|
78
78
|
continue;
|
|
79
|
-
resolved[item.identifier] =
|
|
79
|
+
resolved[item.identifier] = resolveItemSessionControl(args.sectionPolicy, item.policy);
|
|
80
80
|
}
|
|
81
81
|
return resolved;
|
|
82
82
|
}
|
|
83
|
-
/** True when at least one item in the section
|
|
84
|
-
export function
|
|
83
|
+
/** True when at least one item in the section enables item session control. */
|
|
84
|
+
export function isItemSessionControlEnabledInSection(policies) {
|
|
85
85
|
return Object.values(policies).some((policy) => policy.enabled);
|
|
86
86
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ItemTryState, TrySectionSlice } from "./types.js";
|
|
2
|
+
export declare const TRY_SLICE_VERSION: 1;
|
|
3
|
+
export declare function toTrySectionSlice(states: Record<string, ItemTryState>): TrySectionSlice;
|
|
4
|
+
/**
|
|
5
|
+
* Validate a persisted slice against the section it is being restored into.
|
|
6
|
+
*
|
|
7
|
+
* Returns `null` for a slice this build cannot read, which the caller treats as
|
|
8
|
+
* "start Try state clean". Rejection is deliberately narrow: it discards
|
|
9
|
+
* Try progress and nothing else, so a version bump here never costs a
|
|
10
|
+
* learner their responses — item sessions travel in the same snapshot and are
|
|
11
|
+
* normalized separately.
|
|
12
|
+
*
|
|
13
|
+
* An absent slice is `null` too, and indistinguishable from a save made
|
|
14
|
+
* without Try state, which is what keeps existing snapshots valid.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeTrySectionSlice(args: {
|
|
17
|
+
slice: unknown;
|
|
18
|
+
allowedItemIdentifiers: readonly string[];
|
|
19
|
+
}): Record<string, ItemTryState> | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const
|
|
1
|
+
import { ITEM_TRY_STATE_VERSION } from "./state.js";
|
|
2
|
+
export const TRY_SLICE_VERSION = 1;
|
|
3
3
|
const CORRECTNESS_VALUES = [
|
|
4
4
|
"correct",
|
|
5
5
|
"partial",
|
|
@@ -54,14 +54,14 @@ function normalizeItemState(itemIdentifier, value) {
|
|
|
54
54
|
if (!value || typeof value !== "object")
|
|
55
55
|
return null;
|
|
56
56
|
const raw = value;
|
|
57
|
-
if (raw.version !==
|
|
57
|
+
if (raw.version !== ITEM_TRY_STATE_VERSION)
|
|
58
58
|
return null;
|
|
59
59
|
const tryCount = readNonNegativeInteger(raw.tryCount);
|
|
60
60
|
if (tryCount === undefined)
|
|
61
61
|
return null;
|
|
62
62
|
const firstCorrectTry = readNonNegativeInteger(raw.firstCorrectTry);
|
|
63
63
|
return {
|
|
64
|
-
version:
|
|
64
|
+
version: ITEM_TRY_STATE_VERSION,
|
|
65
65
|
itemIdentifier,
|
|
66
66
|
tryCount,
|
|
67
67
|
revealed: raw.revealed === true,
|
|
@@ -72,9 +72,9 @@ function normalizeItemState(itemIdentifier, value) {
|
|
|
72
72
|
revealOverride: normalizeRevealOverride(raw.revealOverride),
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
export function
|
|
75
|
+
export function toTrySectionSlice(states) {
|
|
76
76
|
return {
|
|
77
|
-
version:
|
|
77
|
+
version: TRY_SLICE_VERSION,
|
|
78
78
|
items: { ...states },
|
|
79
79
|
};
|
|
80
80
|
}
|
|
@@ -82,20 +82,20 @@ export function toFormativeSectionSlice(states) {
|
|
|
82
82
|
* Validate a persisted slice against the section it is being restored into.
|
|
83
83
|
*
|
|
84
84
|
* Returns `null` for a slice this build cannot read, which the caller treats as
|
|
85
|
-
* "start
|
|
86
|
-
*
|
|
85
|
+
* "start Try state clean". Rejection is deliberately narrow: it discards
|
|
86
|
+
* Try progress and nothing else, so a version bump here never costs a
|
|
87
87
|
* learner their responses — item sessions travel in the same snapshot and are
|
|
88
88
|
* normalized separately.
|
|
89
89
|
*
|
|
90
|
-
* An absent slice is `null` too, and indistinguishable from a
|
|
91
|
-
*
|
|
90
|
+
* An absent slice is `null` too, and indistinguishable from a save made
|
|
91
|
+
* without Try state, which is what keeps existing snapshots valid.
|
|
92
92
|
*/
|
|
93
|
-
export function
|
|
93
|
+
export function normalizeTrySectionSlice(args) {
|
|
94
94
|
const { slice } = args;
|
|
95
95
|
if (!slice || typeof slice !== "object")
|
|
96
96
|
return null;
|
|
97
97
|
const raw = slice;
|
|
98
|
-
if (raw.version !==
|
|
98
|
+
if (raw.version !== TRY_SLICE_VERSION)
|
|
99
99
|
return null;
|
|
100
100
|
if (!raw.items || typeof raw.items !== "object")
|
|
101
101
|
return null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
3
|
-
export declare function
|
|
1
|
+
import type { FeedbackReveal, ItemTryState, ItemTryView, TryOutcome, ResolvedItemSessionControlPolicy } from "./types.js";
|
|
2
|
+
export declare const ITEM_TRY_STATE_VERSION: 1;
|
|
3
|
+
export declare function createItemTryState(itemIdentifier: string): ItemTryState;
|
|
4
4
|
/**
|
|
5
5
|
* Record one Try.
|
|
6
6
|
*
|
|
@@ -8,24 +8,24 @@ export declare function createFormativeItemState(itemIdentifier: string): Format
|
|
|
8
8
|
* state can be checked, so a second click landing before the projection
|
|
9
9
|
* round-trips is dropped rather than spending a Try.
|
|
10
10
|
*/
|
|
11
|
-
export declare function
|
|
12
|
-
state?:
|
|
11
|
+
export declare function recordTry(args: {
|
|
12
|
+
state?: ItemTryState;
|
|
13
13
|
itemIdentifier: string;
|
|
14
|
-
policy:
|
|
15
|
-
outcome:
|
|
16
|
-
}):
|
|
14
|
+
policy: ResolvedItemSessionControlPolicy;
|
|
15
|
+
outcome: TryOutcome;
|
|
16
|
+
}): ItemTryState;
|
|
17
17
|
/**
|
|
18
18
|
* Dismiss a reveal and reopen the item for editing. Withdrawing `revealed` is
|
|
19
19
|
* what withdraws the env override, so the item returns to the section's own
|
|
20
20
|
* mode.
|
|
21
21
|
*
|
|
22
22
|
* A learner action, so it respects the Try budget: an item with none left keeps
|
|
23
|
-
* its feedback on screen. `
|
|
23
|
+
* its feedback on screen. `hideItemFeedback` is the host-authority version.
|
|
24
24
|
*/
|
|
25
|
-
export declare function
|
|
26
|
-
state?:
|
|
27
|
-
policy:
|
|
28
|
-
}):
|
|
25
|
+
export declare function retryItem(args: {
|
|
26
|
+
state?: ItemTryState;
|
|
27
|
+
policy: ResolvedItemSessionControlPolicy;
|
|
28
|
+
}): ItemTryState | undefined;
|
|
29
29
|
/**
|
|
30
30
|
* Reveal on host authority — a teacher-driven "show the answer".
|
|
31
31
|
*
|
|
@@ -38,23 +38,23 @@ export declare function retryFormativeItem(args: {
|
|
|
38
38
|
* whatever response is there, including none, which is what "show the answer"
|
|
39
39
|
* means before the learner has answered.
|
|
40
40
|
*/
|
|
41
|
-
export declare function
|
|
42
|
-
state?:
|
|
41
|
+
export declare function revealItemFeedback(args: {
|
|
42
|
+
state?: ItemTryState;
|
|
43
43
|
itemIdentifier: string;
|
|
44
|
-
policy:
|
|
45
|
-
feedback: Exclude<
|
|
46
|
-
}):
|
|
44
|
+
policy: ResolvedItemSessionControlPolicy;
|
|
45
|
+
feedback: Exclude<FeedbackReveal, "none">;
|
|
46
|
+
}): ItemTryState;
|
|
47
47
|
/**
|
|
48
48
|
* Withdraw a reveal on host authority. Unlike a learner retry this ignores the
|
|
49
49
|
* Try budget: a host that revealed an item with no Tries left must be able to
|
|
50
50
|
* put it back.
|
|
51
51
|
*/
|
|
52
|
-
export declare function
|
|
53
|
-
state?:
|
|
54
|
-
policy:
|
|
55
|
-
}):
|
|
52
|
+
export declare function hideItemFeedback(args: {
|
|
53
|
+
state?: ItemTryState;
|
|
54
|
+
policy: ResolvedItemSessionControlPolicy;
|
|
55
|
+
}): ItemTryState | undefined;
|
|
56
56
|
/** Derive everything a rendering component needs from policy plus state. */
|
|
57
|
-
export declare function
|
|
58
|
-
policy:
|
|
59
|
-
state?:
|
|
60
|
-
}):
|
|
57
|
+
export declare function resolveItemTryView(args: {
|
|
58
|
+
policy: ResolvedItemSessionControlPolicy;
|
|
59
|
+
state?: ItemTryState;
|
|
60
|
+
}): ItemTryView;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export const
|
|
2
|
-
export function
|
|
1
|
+
export const ITEM_TRY_STATE_VERSION = 1;
|
|
2
|
+
export function createItemTryState(itemIdentifier) {
|
|
3
3
|
return {
|
|
4
|
-
version:
|
|
4
|
+
version: ITEM_TRY_STATE_VERSION,
|
|
5
5
|
itemIdentifier,
|
|
6
6
|
tryCount: 0,
|
|
7
7
|
revealed: false,
|
|
@@ -20,7 +20,7 @@ function shouldReveal(policy, tryCount) {
|
|
|
20
20
|
return false;
|
|
21
21
|
if (policy.revealOn === "on-try")
|
|
22
22
|
return true;
|
|
23
|
-
// Defensive: `
|
|
23
|
+
// Defensive: `resolveItemSessionControl` already coerces `"on-final-try"` to
|
|
24
24
|
// `"on-try"` under unlimited Tries, so this branch only guards a
|
|
25
25
|
// hand-constructed policy.
|
|
26
26
|
if (policy.maxTries === "unlimited")
|
|
@@ -34,8 +34,8 @@ function shouldReveal(policy, tryCount) {
|
|
|
34
34
|
* state can be checked, so a second click landing before the projection
|
|
35
35
|
* round-trips is dropped rather than spending a Try.
|
|
36
36
|
*/
|
|
37
|
-
export function
|
|
38
|
-
const current = args.state ??
|
|
37
|
+
export function recordTry(args) {
|
|
38
|
+
const current = args.state ?? createItemTryState(args.itemIdentifier);
|
|
39
39
|
if (!args.policy.enabled)
|
|
40
40
|
return current;
|
|
41
41
|
const remaining = triesRemaining(args.policy, current.tryCount);
|
|
@@ -44,7 +44,7 @@ export function recordFormativeTry(args) {
|
|
|
44
44
|
const tryCount = current.tryCount + 1;
|
|
45
45
|
return {
|
|
46
46
|
...current,
|
|
47
|
-
version:
|
|
47
|
+
version: ITEM_TRY_STATE_VERSION,
|
|
48
48
|
itemIdentifier: args.itemIdentifier,
|
|
49
49
|
tryCount,
|
|
50
50
|
revealed: shouldReveal(args.policy, tryCount),
|
|
@@ -62,9 +62,9 @@ export function recordFormativeTry(args) {
|
|
|
62
62
|
* mode.
|
|
63
63
|
*
|
|
64
64
|
* A learner action, so it respects the Try budget: an item with none left keeps
|
|
65
|
-
* its feedback on screen. `
|
|
65
|
+
* its feedback on screen. `hideItemFeedback` is the host-authority version.
|
|
66
66
|
*/
|
|
67
|
-
export function
|
|
67
|
+
export function retryItem(args) {
|
|
68
68
|
const current = args.state;
|
|
69
69
|
if (!current || !args.policy.enabled)
|
|
70
70
|
return current;
|
|
@@ -87,8 +87,8 @@ export function retryFormativeItem(args) {
|
|
|
87
87
|
* whatever response is there, including none, which is what "show the answer"
|
|
88
88
|
* means before the learner has answered.
|
|
89
89
|
*/
|
|
90
|
-
export function
|
|
91
|
-
const current = args.state ??
|
|
90
|
+
export function revealItemFeedback(args) {
|
|
91
|
+
const current = args.state ?? createItemTryState(args.itemIdentifier);
|
|
92
92
|
if (!args.policy.enabled)
|
|
93
93
|
return current;
|
|
94
94
|
if (current.revealed && current.revealOverride === args.feedback) {
|
|
@@ -101,7 +101,7 @@ export function revealFormativeItem(args) {
|
|
|
101
101
|
* Try budget: a host that revealed an item with no Tries left must be able to
|
|
102
102
|
* put it back.
|
|
103
103
|
*/
|
|
104
|
-
export function
|
|
104
|
+
export function hideItemFeedback(args) {
|
|
105
105
|
const current = args.state;
|
|
106
106
|
if (!current || !args.policy.enabled)
|
|
107
107
|
return current;
|
|
@@ -119,7 +119,7 @@ function envOverrideFor(policy, revealed, revealOverride) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
/** Derive everything a rendering component needs from policy plus state. */
|
|
122
|
-
export function
|
|
122
|
+
export function resolveItemTryView(args) {
|
|
123
123
|
const { policy } = args;
|
|
124
124
|
const tryCount = args.state?.tryCount ?? 0;
|
|
125
125
|
const revealed = args.state?.revealed === true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Item session control vocabulary.
|
|
3
3
|
*
|
|
4
4
|
* A **Try** is one submitted-for-checking pass over a single item. It is not
|
|
5
5
|
* `TestAttemptSession` (the assessment administration) and not
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* an item). Both of those already own the word "attempt" in this codebase, at
|
|
8
8
|
* two different scopes, which is why this contract does not use it.
|
|
9
9
|
*
|
|
10
|
-
* See `docs/prds/
|
|
10
|
+
* See `docs/prds/item-session-control-contract.md` for the ratified contract and
|
|
11
11
|
* its QTI 3 mapping.
|
|
12
12
|
*/
|
|
13
13
|
/** How many Tries a learner gets on one item. */
|
|
14
|
-
export type
|
|
14
|
+
export type TryLimit = number | "unlimited";
|
|
15
15
|
/**
|
|
16
16
|
* What the learner sees after a Try.
|
|
17
17
|
*
|
|
@@ -20,54 +20,52 @@ export type FormativeTryLimit = number | "unlimited";
|
|
|
20
20
|
* `role: "instructor"`, which is the element convention for additionally
|
|
21
21
|
* revealing the authored correct response.
|
|
22
22
|
*/
|
|
23
|
-
export type
|
|
23
|
+
export type FeedbackReveal = "none" | "correctness" | "solution";
|
|
24
24
|
/** When a reveal happens: after every Try, or only once Tries are spent. */
|
|
25
|
-
export type
|
|
25
|
+
export type FeedbackRevealTiming = "on-try" | "on-final-try";
|
|
26
26
|
/**
|
|
27
|
-
* Authored policy. Lives on `AssessmentSection.
|
|
28
|
-
* default and on `AssessmentItemRef.
|
|
27
|
+
* Authored policy. Lives on `AssessmentSection.itemSessionControl` as the section
|
|
28
|
+
* default and on `AssessmentItemRef.itemSessionControl` as a per-item override.
|
|
29
29
|
*/
|
|
30
|
-
export interface
|
|
30
|
+
export interface ItemSessionControlPolicy {
|
|
31
31
|
/**
|
|
32
32
|
* Absent or `false` leaves delivery exactly as it is without this contract:
|
|
33
33
|
* no control, no projection, no state.
|
|
34
34
|
*/
|
|
35
35
|
enabled?: boolean;
|
|
36
36
|
/** Default 1. */
|
|
37
|
-
maxTries?:
|
|
37
|
+
maxTries?: TryLimit;
|
|
38
38
|
/** Default `"correctness"`. */
|
|
39
|
-
feedback?:
|
|
39
|
+
feedback?: FeedbackReveal;
|
|
40
40
|
/** Default `"on-try"`. */
|
|
41
|
-
revealOn?:
|
|
41
|
+
revealOn?: FeedbackRevealTiming;
|
|
42
42
|
}
|
|
43
|
-
/** Same shape as the section policy; every field overrides independently. */
|
|
44
|
-
export type FormativeItemPolicy = FormativeDeliveryPolicy;
|
|
45
43
|
/** The policy after built-in defaults, section, and item ref are merged. */
|
|
46
|
-
export interface
|
|
44
|
+
export interface ResolvedItemSessionControlPolicy {
|
|
47
45
|
enabled: boolean;
|
|
48
|
-
maxTries:
|
|
49
|
-
feedback:
|
|
50
|
-
revealOn:
|
|
46
|
+
maxTries: TryLimit;
|
|
47
|
+
feedback: FeedbackReveal;
|
|
48
|
+
revealOn: FeedbackRevealTiming;
|
|
51
49
|
}
|
|
52
50
|
/**
|
|
53
51
|
* `"unknown"` is the honest state for an item no loaded controller can score —
|
|
54
52
|
* one holding a rubric element, or one whose element bundle exposes no
|
|
55
53
|
* `outcome`. It is never collapsed into `"incorrect"`.
|
|
56
54
|
*/
|
|
57
|
-
export type
|
|
55
|
+
export type TryCorrectness = "correct" | "partial" | "incorrect" | "unknown";
|
|
58
56
|
/**
|
|
59
57
|
* The subset of an element outcome that aggregation reads. `OutcomeResponse` is
|
|
60
58
|
* assignable to this; the structural type keeps this module free of a
|
|
61
59
|
* dependency on the entity types, which import policy types from here.
|
|
62
60
|
*/
|
|
63
|
-
export interface
|
|
61
|
+
export interface ScoredElementOutcome {
|
|
64
62
|
score?: number;
|
|
65
63
|
max?: number;
|
|
66
64
|
[key: string]: unknown;
|
|
67
65
|
}
|
|
68
66
|
/** The recorded result of one Try. */
|
|
69
|
-
export interface
|
|
70
|
-
correctness:
|
|
67
|
+
export interface TryOutcome {
|
|
68
|
+
correctness: TryCorrectness;
|
|
71
69
|
points?: number;
|
|
72
70
|
max?: number;
|
|
73
71
|
scoredElementCount: number;
|
|
@@ -84,10 +82,10 @@ export interface FormativeTryOutcome {
|
|
|
84
82
|
* A trade: this persists inside the session slice, so a snapshot grows by
|
|
85
83
|
* whatever the elements put in their outcomes — some include a scoring trace.
|
|
86
84
|
*/
|
|
87
|
-
elementOutcomes?:
|
|
85
|
+
elementOutcomes?: ScoredElementOutcome[];
|
|
88
86
|
}
|
|
89
|
-
/** Per-item delivery state. Persisted; see `
|
|
90
|
-
export interface
|
|
87
|
+
/** Per-item delivery state. Persisted; see `TrySectionSlice`. */
|
|
88
|
+
export interface ItemTryState {
|
|
91
89
|
version: 1;
|
|
92
90
|
itemIdentifier: string;
|
|
93
91
|
tryCount: number;
|
|
@@ -98,7 +96,7 @@ export interface FormativeItemState {
|
|
|
98
96
|
* reload restores the screen the learner left.
|
|
99
97
|
*/
|
|
100
98
|
revealed: boolean;
|
|
101
|
-
lastOutcome?:
|
|
99
|
+
lastOutcome?: TryOutcome;
|
|
102
100
|
/** 1-based Try on which the item first scored full credit. Never overwritten. */
|
|
103
101
|
firstCorrectTry?: number;
|
|
104
102
|
/**
|
|
@@ -106,19 +104,19 @@ export interface FormativeItemState {
|
|
|
106
104
|
* than the learner earning it. Overrides the policy's `feedback` for the env
|
|
107
105
|
* projection only — the policy still decides Tries.
|
|
108
106
|
*
|
|
109
|
-
* Set by `
|
|
107
|
+
* Set by `revealItemFeedback`, cleared by `hideItemFeedback` and by a
|
|
110
108
|
* learner retry, so a teacher-driven "show the answer" does not silently
|
|
111
109
|
* upgrade every later reveal on that item.
|
|
112
110
|
*/
|
|
113
|
-
revealOverride?:
|
|
111
|
+
revealOverride?: FeedbackReveal;
|
|
114
112
|
}
|
|
115
|
-
/** The persisted slice, carried on `SectionControllerSessionState.
|
|
116
|
-
export interface
|
|
113
|
+
/** The persisted slice, carried on `SectionControllerSessionState.tries`. */
|
|
114
|
+
export interface TrySectionSlice {
|
|
117
115
|
version: 1;
|
|
118
|
-
items: Record<string,
|
|
116
|
+
items: Record<string, ItemTryState>;
|
|
119
117
|
}
|
|
120
118
|
/** Projected over the section env for one revealed item. */
|
|
121
|
-
export interface
|
|
119
|
+
export interface FeedbackEnvOverride {
|
|
122
120
|
mode: "evaluate";
|
|
123
121
|
role: "student" | "instructor";
|
|
124
122
|
}
|
|
@@ -126,18 +124,18 @@ export interface FormativeEnvOverride {
|
|
|
126
124
|
* What a rendering component needs, derived once so no component reimplements
|
|
127
125
|
* the predicate. Carries no copy — wording belongs to the rendering package.
|
|
128
126
|
*/
|
|
129
|
-
export interface
|
|
127
|
+
export interface ItemTryView {
|
|
130
128
|
enabled: boolean;
|
|
131
129
|
tryCount: number;
|
|
132
130
|
triesRemaining: number | "unlimited";
|
|
133
131
|
canCheck: boolean;
|
|
134
132
|
canRetry: boolean;
|
|
135
133
|
revealed: boolean;
|
|
136
|
-
envOverride?:
|
|
137
|
-
lastOutcome?:
|
|
134
|
+
envOverride?: FeedbackEnvOverride;
|
|
135
|
+
lastOutcome?: TryOutcome;
|
|
138
136
|
}
|
|
139
137
|
/** Section rollup over Try outcomes. */
|
|
140
|
-
export interface
|
|
138
|
+
export interface MasteryRollup {
|
|
141
139
|
version: 1;
|
|
142
140
|
totalItems: number;
|
|
143
141
|
/**
|
|
@@ -153,12 +151,12 @@ export interface FormativeMasteryRollup {
|
|
|
153
151
|
complete: boolean;
|
|
154
152
|
}
|
|
155
153
|
/** The composition-model projection layouts read. */
|
|
156
|
-
export interface
|
|
154
|
+
export interface TrySectionProjection {
|
|
157
155
|
version: 1;
|
|
158
156
|
enabled: boolean;
|
|
159
|
-
policies: Record<string,
|
|
160
|
-
states: Record<string,
|
|
161
|
-
mastery:
|
|
157
|
+
policies: Record<string, ResolvedItemSessionControlPolicy>;
|
|
158
|
+
states: Record<string, ItemTryState>;
|
|
159
|
+
mastery: MasteryRollup;
|
|
162
160
|
}
|
|
163
161
|
/** The two learner actions a rendering component can request. */
|
|
164
|
-
export type
|
|
162
|
+
export type TryAction = "check" | "retry";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Item session control vocabulary.
|
|
3
3
|
*
|
|
4
4
|
* A **Try** is one submitted-for-checking pass over a single item. It is not
|
|
5
5
|
* `TestAttemptSession` (the assessment administration) and not
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* an item). Both of those already own the word "attempt" in this codebase, at
|
|
8
8
|
* two different scopes, which is why this contract does not use it.
|
|
9
9
|
*
|
|
10
|
-
* See `docs/prds/
|
|
10
|
+
* See `docs/prds/item-session-control-contract.md` for the ratified contract and
|
|
11
11
|
* its QTI 3 mapping.
|
|
12
12
|
*/
|
|
13
13
|
export {};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure apart from one adapter factory that is inert until called, so the contract
|
|
6
6
|
* is testable without a browser and an adapter can import it without pulling in a
|
|
7
7
|
* player. Live cue state belongs to `SectionController` in
|
|
8
|
-
* `@pie-players/pie-section-player`, which is where the equivalent
|
|
8
|
+
* `@pie-players/pie-section-player`, which is where the equivalent Try state
|
|
9
9
|
* lives — one rollup over one item set, in one package.
|
|
10
10
|
*
|
|
11
11
|
* Contract: `docs/prds/timed-media-section-contract.md`.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure apart from one adapter factory that is inert until called, so the contract
|
|
6
6
|
* is testable without a browser and an adapter can import it without pulling in a
|
|
7
7
|
* player. Live cue state belongs to `SectionController` in
|
|
8
|
-
* `@pie-players/pie-section-player`, which is where the equivalent
|
|
8
|
+
* `@pie-players/pie-section-player`, which is where the equivalent Try state
|
|
9
9
|
* lives — one rollup over one item set, in one package.
|
|
10
10
|
*
|
|
11
11
|
* Contract: `docs/prds/timed-media-section-contract.md`.
|
|
@@ -28,9 +28,9 @@ export declare function normalizeTimedMediaSectionData(args: {
|
|
|
28
28
|
itemIdentifiers: readonly string[];
|
|
29
29
|
resolveStimulusRenderableId: (stimulusRef: string) => string | undefined;
|
|
30
30
|
/**
|
|
31
|
-
* The Try budget each item's resolved
|
|
31
|
+
* The Try budget each item's resolved item session control policy leaves, so a gate on
|
|
32
32
|
* correctness can be refused where it would strand the learner. Supplied by the
|
|
33
|
-
* caller because
|
|
33
|
+
* caller because item session control policy resolution is that contract's, not this one's.
|
|
34
34
|
*/
|
|
35
35
|
resolveItemTryBudget: (itemIdentifier: string) => TimedMediaItemTryBudget;
|
|
36
36
|
}): TimedMediaValidationResult;
|
|
@@ -240,7 +240,7 @@ export function normalizeTimedMediaSectionData(args) {
|
|
|
240
240
|
// A correctness gate is only passable while the learner still has a Try
|
|
241
241
|
// to spend. Over an item with a finite budget the gate becomes
|
|
242
242
|
// unpassable the moment the budget runs out, and over an item that does
|
|
243
|
-
// not
|
|
243
|
+
// not enable item session control there is never a Try at all — in both cases
|
|
244
244
|
// nothing the learner can do releases playback, and no host action
|
|
245
245
|
// releases it either, because a forced reveal is not a correct answer.
|
|
246
246
|
// Refused here rather than released at runtime: releasing would deliver
|
|
@@ -250,7 +250,7 @@ export function normalizeTimedMediaSectionData(args) {
|
|
|
250
250
|
if (unpassable.length > 0) {
|
|
251
251
|
errors.push({
|
|
252
252
|
code: "gate-requires-unlimited-tries",
|
|
253
|
-
message: `Gate cue "${identifier}" releases on correctness, so every item it names needs
|
|
253
|
+
message: `Gate cue "${identifier}" releases on correctness, so every item it names needs item session control with maxTries: "unlimited" — otherwise a learner who runs out of Tries can never release playback. Offending item refs: ${unpassable.join(", ")}.`,
|
|
254
254
|
cueIdentifier: identifier,
|
|
255
255
|
});
|
|
256
256
|
continue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistence for the timed-media slice.
|
|
3
3
|
*
|
|
4
|
-
* Same posture as the
|
|
4
|
+
* Same posture as the Try slice: an unreadable slice is rejected whole and
|
|
5
5
|
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
6
|
* are normalized separately and survive. A version bump here must never cost a
|
|
7
7
|
* learner their responses.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistence for the timed-media slice.
|
|
3
3
|
*
|
|
4
|
-
* Same posture as the
|
|
4
|
+
* Same posture as the Try slice: an unreadable slice is rejected whole and
|
|
5
5
|
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
6
|
* are normalized separately and survive. A version bump here must never cost a
|
|
7
7
|
* learner their responses.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* See `docs/prds/timed-media-section-contract.md` for the ratified contract and
|
|
10
10
|
* `docs/architecture/timed-media-section.md` for the layer-ownership record.
|
|
11
11
|
*/
|
|
12
|
-
import type {
|
|
12
|
+
import type { TryCorrectness } from "../item-session-control/types.js";
|
|
13
13
|
import type { MediaFragmentRange } from "../types/index.js";
|
|
14
14
|
/**
|
|
15
15
|
* What a cue does to the items it names.
|
|
@@ -23,15 +23,15 @@ export type TimedMediaCueActivation = "reveal" | "gate" | "metadata";
|
|
|
23
23
|
/**
|
|
24
24
|
* The condition over delivery state that releases a gate.
|
|
25
25
|
*
|
|
26
|
-
* Named from the shipped
|
|
27
|
-
* the whole reason
|
|
26
|
+
* Named from the shipped Try vocabulary rather than defined here, which is
|
|
27
|
+
* the whole reason item session control sequenced first — see
|
|
28
28
|
* `docs/adr/0001-formative-delivery-before-timed-media.md`. `"responded"` is the
|
|
29
29
|
* response-only case, and it reads item completion rather than Try state so a
|
|
30
|
-
* gate works in a section that does not
|
|
30
|
+
* gate works in a section that does not enable item session control at all.
|
|
31
31
|
*/
|
|
32
32
|
export type TimedMediaGateCondition = "responded" | "correct" | "partial-or-better";
|
|
33
33
|
/**
|
|
34
|
-
* What a gate does when the item cannot be auto-scored — `
|
|
34
|
+
* What a gate does when the item cannot be auto-scored — `TryCorrectness`
|
|
35
35
|
* of `"unknown"`, which is the honest state of an item holding a rubric element
|
|
36
36
|
* rather than a defect to design around.
|
|
37
37
|
*
|
|
@@ -131,13 +131,13 @@ export interface ResolvedTimedMediaSectionData {
|
|
|
131
131
|
scoringPolicy?: TimedMediaScoringPolicy;
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* What a gated item's resolved
|
|
134
|
+
* What a gated item's resolved item session control policy leaves the learner.
|
|
135
135
|
*
|
|
136
136
|
* Read by validation, not by the reduction: a gate on correctness over an item
|
|
137
137
|
* with a finite Try budget is a dead end, and so is one over an item that does not
|
|
138
|
-
*
|
|
138
|
+
* enable item session control at all — no Try means no correctness, ever.
|
|
139
139
|
*/
|
|
140
|
-
export type TimedMediaItemTryBudget = "unlimited" | "finite" | "
|
|
140
|
+
export type TimedMediaItemTryBudget = "unlimited" | "finite" | "none";
|
|
141
141
|
export interface TimedMediaValidationError {
|
|
142
142
|
/** Machine-readable, so a host can branch without parsing prose. */
|
|
143
143
|
code: "missing-stimulus-ref" | "unresolved-stimulus-ref"
|
|
@@ -219,7 +219,7 @@ export interface MediaTimeSource {
|
|
|
219
219
|
/**
|
|
220
220
|
* The persisted slice, carried on `SectionControllerSessionState.timedMedia`.
|
|
221
221
|
*
|
|
222
|
-
* One shape live and persisted, unlike
|
|
222
|
+
* One shape live and persisted, unlike the Try state's split between a per-item
|
|
223
223
|
* reducer state and a slice: timed-media state is section-scoped, so there is
|
|
224
224
|
* nothing to key.
|
|
225
225
|
*/
|
|
@@ -241,14 +241,14 @@ export interface TimedMediaSectionSessionSlice {
|
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
243
|
* Delivery state a gate condition reads, supplied by the caller so this module
|
|
244
|
-
* stays pure and depends on neither the controller nor the
|
|
244
|
+
* stays pure and depends on neither the controller nor the item session control module's
|
|
245
245
|
* live state.
|
|
246
246
|
*/
|
|
247
247
|
export interface TimedMediaDeliveryState {
|
|
248
248
|
/** Item completion by canonical id: the `"responded"` condition's signal. */
|
|
249
249
|
respondedByItemId: Record<string, boolean>;
|
|
250
250
|
/** Last Try correctness by canonical id; absent means no Try yet. */
|
|
251
|
-
correctnessByItemId: Record<string,
|
|
251
|
+
correctnessByItemId: Record<string, TryCorrectness | undefined>;
|
|
252
252
|
/** Whether every item in the section reports completion. */
|
|
253
253
|
itemsComplete: boolean;
|
|
254
254
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* them. The module depends on nothing beyond this package's own type modules,
|
|
9
9
|
* so front-end and server code can both import it.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { ItemSessionControlPolicy, TrySectionSlice } from "../item-session-control/types.js";
|
|
12
12
|
import type { TimedMediaSectionData, TimedMediaSectionSessionSlice } from "../timed-media/types.js";
|
|
13
13
|
import type { AdaptationLink, CurriculumStandard, LanguagePolicy, SkillLevel } from "./interchange.js";
|
|
14
|
-
export type {
|
|
14
|
+
export type { ItemSessionControlPolicy };
|
|
15
15
|
export type { TimedMediaSectionData };
|
|
16
16
|
export type { AdaptationLink, ContentSubstitution, CurriculumStandard, LanguagePolicy, ServedContentIdentity, SkillLevel, } from "./interchange.js";
|
|
17
17
|
export interface BaseEntity {
|
|
@@ -175,13 +175,13 @@ export interface AssessmentItemRef extends BaseEntity {
|
|
|
175
175
|
/** Item-level settings for tool requirements and customization */
|
|
176
176
|
settings?: ItemSettings;
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
179
|
-
* section's `
|
|
178
|
+
* Item session control override for this item. Every field overrides the
|
|
179
|
+
* section's `itemSessionControl` policy independently, which is the order QTI 3 uses
|
|
180
180
|
* for `qti-item-session-control` on an item ref versus its section.
|
|
181
181
|
*
|
|
182
|
-
* See `docs/prds/
|
|
182
|
+
* See `docs/prds/item-session-control-contract.md`.
|
|
183
183
|
*/
|
|
184
|
-
|
|
184
|
+
itemSessionControl?: ItemSessionControlPolicy;
|
|
185
185
|
}
|
|
186
186
|
export interface AssessmentSection extends BaseEntity {
|
|
187
187
|
id?: string;
|
|
@@ -208,15 +208,15 @@ export interface AssessmentSection extends BaseEntity {
|
|
|
208
208
|
assessmentItemRefs?: AssessmentItemRef[];
|
|
209
209
|
rubricBlocks?: RubricBlock[];
|
|
210
210
|
/**
|
|
211
|
-
*
|
|
211
|
+
* Item session control policy for this section: how many Tries a learner gets
|
|
212
212
|
* on each item, when feedback is revealed, and whether the behavior is on at
|
|
213
213
|
* all. Absent or `enabled: false` delivers exactly as a section without this
|
|
214
214
|
* field — no control, no state, no env override.
|
|
215
215
|
*
|
|
216
|
-
* Individual items override it through `AssessmentItemRef.
|
|
217
|
-
* `docs/prds/
|
|
216
|
+
* Individual items override it through `AssessmentItemRef.itemSessionControl`. See
|
|
217
|
+
* `docs/prds/item-session-control-contract.md`.
|
|
218
218
|
*/
|
|
219
|
-
|
|
219
|
+
itemSessionControl?: ItemSessionControlPolicy;
|
|
220
220
|
/**
|
|
221
221
|
* The section flavor. Absent for every section authored so far, and the only
|
|
222
222
|
* value is `"timed-media"`, which pairs with `timedMedia` below.
|
|
@@ -258,19 +258,19 @@ export interface SectionControllerSessionState {
|
|
|
258
258
|
visitedItemIdentifiers?: string[];
|
|
259
259
|
itemSessions: Record<string, unknown>;
|
|
260
260
|
/**
|
|
261
|
-
*
|
|
261
|
+
* Try state — Try counts and reveal state per item.
|
|
262
262
|
*
|
|
263
263
|
* Optional, and its absence is indistinguishable from a snapshot saved
|
|
264
|
-
* before
|
|
264
|
+
* before item session control existed, which is what keeps existing persisted
|
|
265
265
|
* sessions valid. A slice with an unrecognized `version` is rejected whole
|
|
266
|
-
* and
|
|
267
|
-
* unaffected, so a
|
|
266
|
+
* and Try state starts clean; `itemSessions` in the same snapshot is
|
|
267
|
+
* unaffected, so a Try slice version bump never costs a learner responses.
|
|
268
268
|
*/
|
|
269
|
-
|
|
269
|
+
tries?: TrySectionSlice;
|
|
270
270
|
/**
|
|
271
271
|
* Timed-media delivery state — media progress, cue visits and completions.
|
|
272
272
|
*
|
|
273
|
-
* Same posture as `
|
|
273
|
+
* Same posture as `tries`: optional, absence indistinguishable from a
|
|
274
274
|
* pre-timed-media snapshot, and an unrecognized `version` rejected whole so
|
|
275
275
|
* cue progress restarts while item sessions in the same snapshot are applied
|
|
276
276
|
* untouched.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-players-shared",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared runtime + UI utilities for PIE players",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"types": "./dist/types/index.d.ts",
|
|
46
46
|
"import": "./dist/types/index.js"
|
|
47
47
|
},
|
|
48
|
-
"./
|
|
49
|
-
"types": "./dist/
|
|
50
|
-
"import": "./dist/
|
|
48
|
+
"./item-session-control": {
|
|
49
|
+
"types": "./dist/item-session-control/index.d.ts",
|
|
50
|
+
"import": "./dist/item-session-control/index.js"
|
|
51
51
|
},
|
|
52
52
|
"./timed-media": {
|
|
53
53
|
"types": "./dist/timed-media/index.d.ts",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formative delivery: Try state, feedback reveal, and section mastery.
|
|
3
|
-
*
|
|
4
|
-
* Pure and Node-safe by design — no DOM, no timers, no element registry — so
|
|
5
|
-
* the contract is testable without a browser and an adapter can import it
|
|
6
|
-
* without pulling in a player. Everything that touches the DOM stays in
|
|
7
|
-
* `@pie-players/pie-section-player`.
|
|
8
|
-
*
|
|
9
|
-
* Contract: `docs/prds/formative-delivery-contract.md`.
|
|
10
|
-
*/
|
|
11
|
-
export type { FormativeAction, FormativeCorrectness, FormativeDeliveryPolicy, FormativeEnvOverride, FormativeFeedbackReveal, FormativeItemPolicy, FormativeItemState, FormativeItemView, FormativeMasteryRollup, FormativeRevealTiming, FormativeScoredOutcome, FormativeSectionProjection, FormativeSectionSlice, FormativeTryLimit, FormativeTryOutcome, ResolvedFormativePolicy, } from "./types.js";
|
|
12
|
-
export { FORMATIVE_POLICY_DEFAULTS, isFormativeSectionEnabled, resolveFormativePolicies, resolveFormativePolicy, } from "./policy.js";
|
|
13
|
-
export { aggregateFormativeOutcome } from "./outcome.js";
|
|
14
|
-
export { FORMATIVE_ITEM_STATE_VERSION, createFormativeItemState, hideFormativeItem, recordFormativeTry, resolveFormativeItemView, retryFormativeItem, revealFormativeItem, } from "./state.js";
|
|
15
|
-
export { FORMATIVE_MASTERY_VERSION, rollupFormativeMastery } from "./mastery.js";
|
|
16
|
-
export { FORMATIVE_SLICE_VERSION, normalizeFormativeSectionSlice, toFormativeSectionSlice, } from "./session.js";
|
package/dist/formative/index.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { FORMATIVE_POLICY_DEFAULTS, isFormativeSectionEnabled, resolveFormativePolicies, resolveFormativePolicy, } from "./policy.js";
|
|
2
|
-
export { aggregateFormativeOutcome } from "./outcome.js";
|
|
3
|
-
export { FORMATIVE_ITEM_STATE_VERSION, createFormativeItemState, hideFormativeItem, recordFormativeTry, resolveFormativeItemView, retryFormativeItem, revealFormativeItem, } from "./state.js";
|
|
4
|
-
export { FORMATIVE_MASTERY_VERSION, rollupFormativeMastery } from "./mastery.js";
|
|
5
|
-
export { FORMATIVE_SLICE_VERSION, normalizeFormativeSectionSlice, toFormativeSectionSlice, } from "./session.js";
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { FormativeItemState, FormativeSectionSlice } from "./types.js";
|
|
2
|
-
export declare const FORMATIVE_SLICE_VERSION: 1;
|
|
3
|
-
export declare function toFormativeSectionSlice(states: Record<string, FormativeItemState>): FormativeSectionSlice;
|
|
4
|
-
/**
|
|
5
|
-
* Validate a persisted slice against the section it is being restored into.
|
|
6
|
-
*
|
|
7
|
-
* Returns `null` for a slice this build cannot read, which the caller treats as
|
|
8
|
-
* "start formative state clean". Rejection is deliberately narrow: it discards
|
|
9
|
-
* formative progress and nothing else, so a version bump here never costs a
|
|
10
|
-
* learner their responses — item sessions travel in the same snapshot and are
|
|
11
|
-
* normalized separately.
|
|
12
|
-
*
|
|
13
|
-
* An absent slice is `null` too, and indistinguishable from a pre-formative
|
|
14
|
-
* save, which is what keeps existing snapshots valid.
|
|
15
|
-
*/
|
|
16
|
-
export declare function normalizeFormativeSectionSlice(args: {
|
|
17
|
-
slice: unknown;
|
|
18
|
-
allowedItemIdentifiers: readonly string[];
|
|
19
|
-
}): Record<string, FormativeItemState> | null;
|