chore: establish learning module baseline

This commit is contained in:
inman committed 2026-08-16 14:58:47 +08:00
commit 2d04197f3f
2409 files changed
+533050

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
// 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;
}