Files
2026-08-16 14:58:47 +08:00

152 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}