// 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 | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : 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; }