课程内容 - 重写全部 21 个 A1 单元的听读材料,把本单元理解词织进听/读文本, 单元内理解词复现率从约 20% 提升到约 77%(各单元 56–96%)。 - 修正 U01 房间号与机场大巴同为 thirty 的撞车(改为 17/30/40)。 - 校验器容差按级别读取(A1 为 8%),materials 覆盖率、字数、 选项子串等校验全部通过;course_content_test 通过。 黑夜模式 - app_theme 拆分明/暗两套调色板,AppColors 随亮度切换; main 用 theme/darkTheme/themeMode + builder 镜像已解析亮度; 主题偏好持久化到快照;进度页新增“外观主题”切换。 文档 - COURSE-PACK-JSON.md 更新 A1 词池覆盖(840/933)与材料复现约定。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
2.3 KiB
Dart
63 lines
2.3 KiB
Dart
// 校验 assets/courses 下的单元内容包并报告词池覆盖。
|
|
//
|
|
// dart run tool/courses/validate.dart # 报告错误和覆盖率
|
|
// dart run tool/courses/validate.dart --uncovered A1 # 列出未覆盖的词
|
|
// dart run tool/courses/validate.dart --levels A1-U01 # 打印条目的计算等级
|
|
import 'dart:io';
|
|
|
|
import 'package:kouyu_english/core/a0_core.dart';
|
|
|
|
import 'course_validator.dart';
|
|
|
|
void main(List<String> args) {
|
|
final validator = CourseValidator(
|
|
projectRoot: Directory.current,
|
|
a0Texts: [
|
|
// 去掉 [name] 这类槽位占位符,它们不是教过的词。
|
|
for (final item in a0CoreItems.values)
|
|
item.replaceAll(RegExp(r'\[[^\]]*\]'), ''),
|
|
for (final sentence in a0DictationSentences.values) sentence.sentence,
|
|
],
|
|
);
|
|
final report = validator.validate();
|
|
|
|
for (final issue in report.errors) {
|
|
stdout.writeln('错误 $issue');
|
|
}
|
|
if (report.missingUnits.isNotEmpty) {
|
|
stdout.writeln('待编写 ${report.missingUnits.length} 个单元:'
|
|
'${report.missingUnits.first} … ${report.missingUnits.last}');
|
|
}
|
|
for (final coverage in report.coverage.values) {
|
|
stdout.writeln('${coverage.level} 词池覆盖 ${coverage.covered}/${coverage.poolSize} '
|
|
'(${(coverage.ratio * 100).toStringAsFixed(1)}%)'
|
|
'${coverage.unitsReady ? '' : ',单元未写完'}');
|
|
}
|
|
|
|
final uncoveredIndex = args.indexOf('--uncovered');
|
|
if (uncoveredIndex >= 0 && uncoveredIndex + 1 < args.length) {
|
|
final coverage = report.coverage[args[uncoveredIndex + 1]];
|
|
final byTopic = <String, List<String>>{};
|
|
for (final word in coverage?.uncovered ?? const <PoolWord>[]) {
|
|
byTopic.putIfAbsent(word.topic.isEmpty ? '(无主题)' : word.topic, () => [])
|
|
.add(word.headword);
|
|
}
|
|
final topics = byTopic.keys.toList()
|
|
..sort((a, b) => byTopic[b]!.length - byTopic[a]!.length);
|
|
for (final topic in topics) {
|
|
stdout.writeln('\n[$topic] ${byTopic[topic]!.length}');
|
|
stdout.writeln(byTopic[topic]!.join(', '));
|
|
}
|
|
}
|
|
|
|
final levelsIndex = args.indexOf('--levels');
|
|
if (levelsIndex >= 0 && levelsIndex + 1 < args.length) {
|
|
final levels = report.itemLevels[args[levelsIndex + 1]] ?? const {};
|
|
for (final MapEntry(:key, :value) in levels.entries) {
|
|
stdout.writeln('$key\t$value');
|
|
}
|
|
}
|
|
|
|
exitCode = report.errors.isEmpty ? 0 : 1;
|
|
}
|