chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user