152 lines
5.8 KiB
TypeScript
152 lines
5.8 KiB
TypeScript
// Read-time compatibility for course records created before the explicit
|
||
// Layer-1 generation contract was introduced. New framework generations are
|
||
// validated strictly; persisted legacy records receive deterministic defaults
|
||
// here so they can still be reviewed, resumed, and regenerated.
|
||
|
||
import type {
|
||
CourseContinuityContract,
|
||
CourseFramework,
|
||
CourseModuleSpec,
|
||
CourseRecord,
|
||
} from './types';
|
||
|
||
const LEGACY_TARGET_AUDIENCE = '以原始课程需求中描述的学习者为准';
|
||
|
||
const LEGACY_CONTINUITY_CONTRACT: CourseContinuityContract = {
|
||
terminology: ['同一概念在所有模块中使用一致术语,并遵循课程语言指令'],
|
||
teachingStyle: '所有模块使用一致的讲解结构、示例风格和互动节奏',
|
||
difficultyProgression: '按模块顺序由基础到应用逐步提升,后续模块建立在前序知识之上',
|
||
assessmentStrategy: '围绕可观察学习目标进行形成性检查,并在后续模块复用关键能力',
|
||
};
|
||
|
||
function asObject(value: unknown): Record<string, unknown> | null {
|
||
return value && typeof value === 'object' && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: null;
|
||
}
|
||
|
||
function asString(value: unknown): string {
|
||
return typeof value === 'string' ? value.trim() : '';
|
||
}
|
||
|
||
function asStringArray(value: unknown): string[] {
|
||
if (Array.isArray(value)) {
|
||
return value
|
||
.filter((item): item is string => typeof item === 'string')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
const single = asString(value);
|
||
return single ? [single] : [];
|
||
}
|
||
|
||
function fallbackGenerationPrompt(
|
||
title: string,
|
||
description: string,
|
||
learningObjectives: string[],
|
||
): string {
|
||
const objectives = learningObjectives.length
|
||
? `学习完成后应实现:${learningObjectives.join(';')}。`
|
||
: '';
|
||
return `围绕“${title}”生成一份完整、可独立学习的模块课件。重点覆盖:${description}。${objectives}`;
|
||
}
|
||
|
||
function normalizeLegacyModule(value: unknown, position: number): CourseModuleSpec | null {
|
||
const moduleObj = asObject(value);
|
||
if (!moduleObj) return null;
|
||
|
||
const title = asString(moduleObj.title) || `模块 ${position + 1}`;
|
||
const description = asString(moduleObj.description) || title;
|
||
const learningObjectives = asStringArray(moduleObj.learningObjectives);
|
||
const prerequisites = asString(moduleObj.prerequisites);
|
||
const incomingKnowledge = asStringArray(moduleObj.incomingKnowledge);
|
||
const outgoingKnowledge = asStringArray(moduleObj.outgoingKnowledge);
|
||
const rawMinutes = moduleObj.estimatedMinutes;
|
||
const estimatedMinutes =
|
||
typeof rawMinutes === 'number' && Number.isFinite(rawMinutes)
|
||
? Math.min(Math.max(Math.round(rawMinutes), 5), 60)
|
||
: undefined;
|
||
|
||
return {
|
||
index:
|
||
typeof moduleObj.index === 'number' &&
|
||
Number.isInteger(moduleObj.index) &&
|
||
moduleObj.index > 0
|
||
? moduleObj.index
|
||
: position + 1,
|
||
title,
|
||
description,
|
||
learningObjectives: learningObjectives.length ? learningObjectives : [description],
|
||
generationPrompt:
|
||
asString(moduleObj.generationPrompt) ||
|
||
fallbackGenerationPrompt(
|
||
title,
|
||
description,
|
||
learningObjectives.length ? learningObjectives : [description],
|
||
),
|
||
...(prerequisites ? { prerequisites } : {}),
|
||
incomingKnowledge:
|
||
incomingKnowledge.length > 0 ? incomingKnowledge : prerequisites ? [prerequisites] : [],
|
||
outgoingKnowledge:
|
||
outgoingKnowledge.length > 0
|
||
? outgoingKnowledge
|
||
: learningObjectives.length
|
||
? learningObjectives
|
||
: [description],
|
||
excludedTopics: asStringArray(moduleObj.excludedTopics),
|
||
...(estimatedMinutes ? { estimatedMinutes } : {}),
|
||
};
|
||
}
|
||
|
||
/** Upgrade a persisted framework without weakening validation of new AI output. */
|
||
export function normalizePersistedCourseFramework(value: unknown): CourseFramework | null {
|
||
const framework = asObject(value);
|
||
if (!framework || !Array.isArray(framework.modules)) return null;
|
||
|
||
const modules = framework.modules
|
||
.map(normalizeLegacyModule)
|
||
.filter((module): module is CourseModuleSpec => module !== null)
|
||
.map((module, index) => ({ ...module, index: index + 1 }));
|
||
if (modules.length === 0) return null;
|
||
|
||
const courseTitle = asString(framework.courseTitle);
|
||
const languageDirective = asString(framework.languageDirective);
|
||
const summary = asString(framework.summary);
|
||
if (!courseTitle || !languageDirective || !summary) return null;
|
||
|
||
const courseGoals = asStringArray(framework.courseGoals);
|
||
const targetAudience = asString(framework.targetAudience) || LEGACY_TARGET_AUDIENCE;
|
||
const rawContinuity = asObject(framework.continuityContract);
|
||
const terminology = asStringArray(rawContinuity?.terminology);
|
||
|
||
return {
|
||
courseTitle,
|
||
languageDirective,
|
||
targetAudience,
|
||
summary,
|
||
courseGoals:
|
||
courseGoals.length > 0
|
||
? courseGoals
|
||
: Array.from(new Set(modules.flatMap((module) => module.learningObjectives))).slice(0, 8),
|
||
continuityContract: {
|
||
terminology: terminology.length > 0 ? terminology : LEGACY_CONTINUITY_CONTRACT.terminology,
|
||
teachingStyle:
|
||
asString(rawContinuity?.teachingStyle) || LEGACY_CONTINUITY_CONTRACT.teachingStyle,
|
||
difficultyProgression:
|
||
asString(rawContinuity?.difficultyProgression) ||
|
||
LEGACY_CONTINUITY_CONTRACT.difficultyProgression,
|
||
assessmentStrategy:
|
||
asString(rawContinuity?.assessmentStrategy) ||
|
||
LEGACY_CONTINUITY_CONTRACT.assessmentStrategy,
|
||
},
|
||
modules,
|
||
};
|
||
}
|
||
|
||
/** Preserve the record envelope while upgrading only its optional framework. */
|
||
export function normalizePersistedCourseRecord(record: CourseRecord): CourseRecord {
|
||
if (!record.framework) return record;
|
||
const framework = normalizePersistedCourseFramework(record.framework);
|
||
return framework ? { ...record, framework } : record;
|
||
}
|