Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 31x 31x 31x 6738x 352x 64x 187x 187x 187x 187x 187x 187x 187x 187x 53x 187x 1078x 982x 3x 3x 3x 3x 3x 3x 3x 3x 3x 138x 138x 138x 54x 35x 35x 35x 136x 45x 53x 45x 45x 45x | import type { CurriculumTopic, TopicDeepening } from "./curriculum-schema";
import type { Observation } from "./learning-planner";
export const DIDACTIC_PHASES = ["LEARN", "DEEPEN", "EXPAND"] as const;
export type DidacticPhase = typeof DIDACTIC_PHASES[number];
export const PROGRESSION_BLOCKED_REASONS = ["content-exhausted"] as const;
export type ProgressionBlockedReason = typeof PROGRESSION_BLOCKED_REASONS[number];
export interface TutorProgressionState {
revision: string;
activeTopicId?: string;
phase?: DidacticPhase;
masteredTopicIds: string[];
masteryEvidence: Record<string, Observation[]>;
postMasteryEvidence: Record<string, Observation[]>;
retainedPhases: Record<string, "DEEPEN" | "EXPAND">;
usedExpansionTargetTopicIds: Record<string, string[]>;
progressionBlockedReason?: ProgressionBlockedReason;
}
export const DEFAULT_DEEPENING: TopicDeepening = {
minimumSuccessfulProbes: 2,
successRatingAtLeast: 4,
requiredQuestionKinds: ["transfer"],
recentWeakAnswersAllowed: 0,
};
export function createTutorProgressionState(revision: string): TutorProgressionState {
return {
revision,
masteredTopicIds: [],
masteryEvidence: {},
postMasteryEvidence: {},
retainedPhases: {},
usedExpansionTargetTopicIds: {},
};
}
export function cloneTutorProgressionState(state: TutorProgressionState): TutorProgressionState {
return {
...state,
masteredTopicIds: [...state.masteredTopicIds],
masteryEvidence: cloneEvidence(state.masteryEvidence),
postMasteryEvidence: cloneEvidence(state.postMasteryEvidence),
retainedPhases: { ...state.retainedPhases },
usedExpansionTargetTopicIds: Object.fromEntries(
Object.entries(state.usedExpansionTargetTopicIds).map(([topicId, targetIds]) => [topicId, [...targetIds]]),
),
};
}
export function commitTutorProgressionState(target: TutorProgressionState, source: TutorProgressionState): void {
target.revision = source.revision;
target.activeTopicId = source.activeTopicId;
target.phase = source.phase;
target.masteredTopicIds = [...source.masteredTopicIds];
target.masteryEvidence = cloneEvidence(source.masteryEvidence);
target.postMasteryEvidence = cloneEvidence(source.postMasteryEvidence);
target.retainedPhases = { ...source.retainedPhases };
target.usedExpansionTargetTopicIds = Object.fromEntries(
Object.entries(source.usedExpansionTargetTopicIds).map(([topicId, targetIds]) => [topicId, [...targetIds]]),
);
target.progressionBlockedReason = source.progressionBlockedReason;
}
function cloneEvidence(evidence: Record<string, Observation[]>): Record<string, Observation[]> {
return Object.fromEntries(
Object.entries(evidence).map(([topicId, observations]) => [topicId, observations.map((observation) => ({ ...observation }))]),
);
}
export function resetTutorProgressionState(state: TutorProgressionState, revision = state.revision): void {
state.activeTopicId = undefined;
state.phase = undefined;
state.masteredTopicIds.splice(0, state.masteredTopicIds.length);
state.masteryEvidence = {};
state.postMasteryEvidence = {};
state.retainedPhases = {};
state.usedExpansionTargetTopicIds = {};
state.progressionBlockedReason = undefined;
state.revision = revision;
}
export function appendEvidence(
state: TutorProgressionState,
collection: "masteryEvidence" | "postMasteryEvidence",
topicId: string,
observation: Observation,
): void {
const entries = state[collection][topicId] ?? [];
entries.push(observation);
state[collection][topicId] = entries;
}
export function markTopicMastered(state: TutorProgressionState, topicId: string): void {
Eif (!state.masteredTopicIds.includes(topicId)) state.masteredTopicIds.push(topicId);
}
export function markExpansionTargetUsed(state: TutorProgressionState, sourceTopicId: string, targetTopicId: string): void {
const targets = state.usedExpansionTargetTopicIds[sourceTopicId] ?? [];
Eif (!targets.includes(targetTopicId)) targets.push(targetTopicId);
state.usedExpansionTargetTopicIds[sourceTopicId] = targets;
}
export function deepeningCriteria(topic: CurriculumTopic): TopicDeepening {
return topic.schemaVersion === 2 && topic.deepening ? topic.deepening : DEFAULT_DEEPENING;
}
export function hasMetDeepeningCriteria(topic: CurriculumTopic, observations: readonly Observation[]): boolean {
const criteria = deepeningCriteria(topic);
const successful = observations.filter(({ rating }) => rating >= criteria.successRatingAtLeast);
const weakTrailing = [...observations].reverse().findIndex(({ rating }) => rating > 2);
const recentWeakCount = weakTrailing < 0 ? observations.length : weakTrailing;
return successful.length >= criteria.minimumSuccessfulProbes
&& criteria.requiredQuestionKinds.every((kind) => successful.some((observation) => observation.kind === kind))
&& recentWeakCount <= criteria.recentWeakAnswersAllowed;
}
|