灯光模拟HarmonyOS应用实战-71-科一种子定义了十组却没有进入生成链:用GenerationPlan说明启用、替代与退役
灯光模拟HarmonyOS应用实战-71-科一种子定义了十组却没有进入生成链:用GenerationPlan说明启用、替代与退役
题库文件里已经写了科一的十组生成种子:近光、会车、跟车、远近交替、雾天、故障处理等场景一应俱全。维护者自然会认为这些种子正在扩充科一题库。可继续追到 buildBuiltinQuestions(),实际生成调用只有科二和科四,科一仍只追加手工灯光题。
这类问题比“少写了一行函数调用”更难治理。也许科一种子本来准备启用,也许已被 42 道手工题替代,也许只是旧方案退役后忘了删除。源码没有记录决策,任何人都只能猜。GenerationPlan 的作用就是把每个科目的来源、状态、数量策略、替代关系和退役原因集中写清,让配置存在与运行启用成为两件可审查的事实。

接下来先还原当前链路,再把“启用、替代、退役”转成可执行计划和生成报告,并设计阻止孤立种子再次出现的验证门禁。
一、科一种子目录确实定义了十组业务场景
QuestionBank.ets 中的 SUBJECT_ONE_SEEDS 从“夜间在照明良好的城市道路上行驶”开始,到“仪表盘蓝色前照灯图标亮起”结束,共十个 GeneratedQuestionSeed。每项包含分类、场景、正确做法、三个干扰项和解析。
const SUBJECT_ONE_SEEDS: GeneratedQuestionSeed[] = [
{
category: '使用规则',
scenario: '夜间在照明良好的城市道路上行驶',
correct: '使用近光灯',
wrongOne: '持续使用远光灯',
wrongTwo: '只开启危险报警闪光灯',
wrongThree: '关闭全部灯光',
explanation: '照明良好的道路应使用近光灯,远光灯容易影响其他车辆。'
},
// 其余场景:会车、跟车、远近交替、路口、恶劣天气、
// 故障处理、临时停车、转向提示、灯光标志
];
同文件的 buildGeneratedTitle() 还专门处理 subject === SUBJECT_ONE,getGeneratedSeed() 也会为科一查询 SUBJECT_ONE_SEEDS。这些代码说明生成器具备理解科一种子的分支,却不能证明内置题库实际创建了科一自动题。
| 代码事实 | 能说明什么 | 不能说明什么 |
|---|---|---|
| 定义 10 颗科一种子 | 配置内容存在 | 配置已经启用 |
| 标题函数有科一分支 | 生成器可处理该科目 | 构建入口一定调用 |
| 规范化可查询科一种子 | 若收到对应自动题可尝试反查 | 内置题库已经产生自动题 |
| 页面能筛选科一 | 科一题可展示与练习 | 题目来自哪条生成路径 |
二、buildBuiltinQuestions只启动科二和科四生成

当前内置题库的构建顺序很明确:先追加 SUBJECT_ONE_LIGHT_QUESTIONS,再追加 BASE_QUESTIONS 中非科一题,最后分别为科二和科四生成 1000 条目标数量。
function buildBuiltinQuestions(): QuestionItem[] {
const questions: QuestionItem[] = [];
for (let index = 0;
index < SUBJECT_ONE_LIGHT_QUESTIONS.length; index++) {
appendUniqueQuestion(
questions,
SUBJECT_ONE_LIGHT_QUESTIONS[index]
);
}
for (let index = 0; index < BASE_QUESTIONS.length; index++) {
if (BASE_QUESTIONS[index].subject !== SUBJECT_ONE) {
appendUniqueQuestion(questions, BASE_QUESTIONS[index]);
}
}
appendGeneratedSubjectQuestions(
questions, SUBJECT_TWO, 's2_auto',
SUBJECT_TWO_SEEDS, GENERATED_SUBJECT_COUNT
);
appendGeneratedSubjectQuestions(
questions, SUBJECT_FOUR, 's4_auto',
SUBJECT_FOUR_SEEDS, GENERATED_SUBJECT_COUNT
);
return questions;
}
这里没有以 SUBJECT_ONE 调用 appendGeneratedSubjectQuestions()。因此当前内置构建链中,科一来源是 SubjectOneLightBank.ets 的 42 道手工题;十组生成种子没有进入该链。不能仅凭变量被其他函数引用就把它们统计为“已生成题库”。
三、缺一行调用不等于应该直接补一行
最直接的改法似乎是追加:
appendGeneratedSubjectQuestions(
questions,
SUBJECT_ONE,
's1_auto',
SUBJECT_ONE_SEEDS,
GENERATED_SUBJECT_COUNT
);
但在明确产品决策前,这一行可能引入新的问题:科一现有手工题与生成场景重复;搜索排序和抽样池突然扩大;自动题 ID 成为新的历史引用;此前只审核过手工题内容,生成变体尚未逐场景确认。
正确问题不是“能不能调用”,而是“这些种子处于什么生命周期”。至少存在三种合理解释:
| 状态 | 业务含义 | 生成器行为 |
|---|---|---|
启用 ENABLED | 该目录是当前正式来源 | 按批准策略生成并报告 |
替代 REPLACED | 内容由另一目录接管 | 不生成,指向替代来源 |
退役 RETIRED | 旧能力停止维护 | 不生成,保留原因和最后版本 |
如果十组种子只是未来草案,可以再加 DRAFT。草案不应混在生产构建文件里让人误判;它应位于明确的试验目录,或在计划中声明不可进入发布题库。
四、GenerationPlan集中记录来源与生命周期

计划条目要引用稳定目录 ID,而不是直接散落数组变量。状态、数量、前缀和替代关系都在一处表达。
export enum GenerationLifecycle {
ENABLED = 'enabled',
REPLACED = 'replaced',
RETIRED = 'retired',
DRAFT = 'draft'
}
export interface GenerationPlanEntry {
subject: string;
catalogId: string;
lifecycle: GenerationLifecycle;
targetCount: number;
idPrefix: string;
replacedByCatalogId: string;
reasonCode: string;
planVersion: number;
}
targetCount 只在 ENABLED 时有效。REPLACED 必须提供 replacedByCatalogId,RETIRED 必须提供原因码,DRAFT 则不能被正式构建入口消费。planVersion 在来源或生成策略改变时递增,不能直接借用应用版本代替。
计划不决定每颗题目的稳定身份和规则覆盖,那些仍由独立协议负责。它只回答“哪个目录被哪条构建链以什么状态消费”。
五、先把当前行为编码成计划,再讨论是否启用科一
迁移第一步应忠实描述现状,而不是顺手改变产物。当前计划可以把科一手工目录设为启用,把科一种子目录设为 DRAFT、REPLACED 或 RETIRED,具体状态必须由维护者确认。
const GENERATION_PLAN_V1: GenerationPlanEntry[] = [
{
subject: SUBJECT_ONE,
catalogId: 'subject1.manual.light.v1',
lifecycle: GenerationLifecycle.ENABLED,
targetCount: 42,
idPrefix: 'doc_s1_light',
replacedByCatalogId: '',
reasonCode: 'CURRENT_MANUAL_CATALOG',
planVersion: 1
},
{
subject: SUBJECT_ONE,
catalogId: 'subject1.generated.seed.v1',
lifecycle: GenerationLifecycle.DRAFT,
targetCount: 0,
idPrefix: 's1_auto',
replacedByCatalogId: '',
reasonCode: 'AWAITING_CONTENT_REVIEW',
planVersion: 1
}
];
第二条的 DRAFT 和原因只是示例,不能冒充当前团队已经作出的决定。若实际结论是“42 道手工题替代十组生成种子”,就改为 REPLACED 并指向手工目录;若准备启用,则必须先完成内容审查、重复分析、ID 与迁移方案,再改为 ENABLED。
科二和科四也要进入同一计划,避免它只为修补科一特例而存在。
六、构建器只消费ENABLED条目,并返回生成报告
正式入口遍历计划,而不是手写两次函数调用。任何未启用目录都不会生成,同时报告其生命周期和原因。
export interface GenerationReportItem {
catalogId: string;
lifecycle: GenerationLifecycle;
requestedCount: number;
appendedCount: number;
duplicateSkippedCount: number;
reasonCode: string;
}
function executeGenerationPlan(
plan: GenerationPlanEntry[],
registry: QuestionCatalogRegistry
): GenerationReportItem[] {
const report: GenerationReportItem[] = [];
for (let index: number = 0; index < plan.length; index++) {
const entry: GenerationPlanEntry = plan[index];
if (entry.lifecycle !== GenerationLifecycle.ENABLED) {
report.push(skippedReport(entry));
continue;
}
report.push(appendEnabledCatalog(entry, registry));
}
return report;
}
当前 appendUniqueQuestion() 按科目、分类和标题跳过重复项,所以“请求生成 1000 条”不必然等于“新增 1000 条”。报告必须同时保存 requestedCount、appendedCount 和 duplicateSkippedCount,不能把目标数量当最终产量。
若 ENABLED 目录在注册表中不存在、种子数组为空或前缀冲突,构建应明确失败;若 DRAFT 被正式入口请求,也应拒绝,而不是自动升级为启用。
七、替代与退役必须携带迁移影响
目录从启用变为替代或退役时,旧题 ID、错题历史和进行中会话仍可能引用它。计划需要配套影响清单,而不是从构建入口直接删除来源。
export interface CatalogTransition {
catalogId: string;
from: GenerationLifecycle;
to: GenerationLifecycle;
effectivePlanVersion: number;
existingReferencePolicy: string;
replacementCatalogId: string;
}
const transition: CatalogTransition = {
catalogId: 'subject1.generated.seed.v1',
from: GenerationLifecycle.ENABLED,
to: GenerationLifecycle.REPLACED,
effectivePlanVersion: 3,
existingReferencePolicy: 'READ_ONLY_SNAPSHOT',
replacementCatalogId: 'subject1.manual.light.v2'
};
READ_ONLY_SNAPSHOT 表示旧记录保留当时题目快照,不能自动映射到语义相近的新题。若团队已有稳定的一对一映射,可以采用显式映射表;无法确认时就记录 UNRESOLVED,不要按标题相似度猜替代项。
| 转换 | 新生成 | 旧引用 | 必需说明 |
|---|---|---|---|
| DRAFT → ENABLED | 开始 | 建立新身份 | 内容与容量评审结果 |
| ENABLED → REPLACED | 停止旧目录 | 映射或只读快照 | 替代目录及生效版本 |
| ENABLED → RETIRED | 停止 | 保留旧读取路径 | 退役原因与保留期限 |
| REPLACED → ENABLED | 谨慎恢复 | 核对两套引用 | 恢复理由和冲突处理 |
八、孤立目录应在构建前被主动发现
配置存在但没有计划条目,是这次问题的根源之一。注册表中的每个正式目录都必须被计划引用一次;计划也不能引用不存在的目录。
function validatePlanCoverage(plan: GenerationPlanEntry[],
registryIds: string[]): string[] {
const errors: string[] = [];
for (let index: number = 0;
index < registryIds.length; index++) {
if (!hasPlanEntry(plan, registryIds[index])) {
errors.push('UNPLANNED_CATALOG:' + registryIds[index]);
}
}
for (let index: number = 0; index < plan.length; index++) {
if (registryIds.indexOf(plan[index].catalogId) < 0) {
errors.push('MISSING_CATALOG:' + plan[index].catalogId);
}
}
return errors;
}
还应校验同一 catalogId 只出现一次、启用条目的 targetCount 为正、非启用条目不会携带可执行数量、替代目录不能指向自己、替代链不能成环、ID 前缀在同一题库范围内唯一。
检查结果要在题库构建前返回,避免页面运行后才发现某个科目数量异常。
九、验证矩阵要覆盖计划状态与实际产物
测试不能只断言常量数组长度。它要读取生成报告和最终题库,证明每个计划状态产生了预期行为。
| 用例 | 计划状态 | 关键断言 |
|---|---|---|
| G01 | 科一种子 DRAFT | 最终题库没有 s1_auto,报告写明跳过原因 |
| G02 | 科一种子 ENABLED | 只生成批准数量,前缀唯一 |
| G03 | 科一种子 REPLACED | 不生成,替代目录存在且启用 |
| G04 | 科一种子 RETIRED | 不生成,旧读取策略存在 |
| G05 | 注册表新增目录无计划 | 构建前返回 UNPLANNED_CATALOG |
| G06 | 计划引用缺失目录 | 返回 MISSING_CATALOG |
| G07 | 两目录使用同一前缀 | 拒绝生成 |
| G08 | 重复题被跳过 | 报告的请求数、追加数、跳过数守恒 |
| G09 | 替代链成环 | 计划校验失败 |
| G10 | 同一 V1 计划执行两次 | 生成身份和报告保持一致 |
最终还要按科目统计手工、生成、替代和退役数量。总题数只说明容量,不能替代目录生命周期、内容审查和规则覆盖结论。
十、验证清单、排障表与事实边界
- 每个题目目录都有唯一
catalogId。 - 每个正式目录恰好出现在一个计划条目中。
-
ENABLED、REPLACED、RETIRED和DRAFT语义固定。 - 非启用目录不会进入正式生成链。
- 替代条目指向存在且经过确认的目录。
- 退役条目说明旧引用读取策略。
- 启用条目的目标数量与 ID 前缀合法且无冲突。
- 生成报告区分请求、追加与重复跳过数量。
- 计划变更带版本和迁移影响清单。
- 科目总数按来源拆分,不把种子存在当作已生成。
- 旧错题与历史引用不会按相似标题自动迁移。
- 计划和注册表覆盖在题库构建前核对。
| 现象 | 先核对 | 常见原因 | 修复方向 |
|---|---|---|---|
| 种子写了却搜不到题 | 生成报告 | 目录没有启用条目 | 明确生命周期后再决定接入 |
| 加一行后科一题暴增 | 计划数量与重复项 | 直接启用未评审目录 | 先做影子报告与内容审查 |
| 目标 1000 条实际更少 | 重复跳过数 | 把请求数当追加数 | 输出三项守恒报告 |
| 删除目录后旧错题打不开 | 退役引用策略 | 直接移除读取能力 | 保留快照或显式映射 |
| 两套自动题 ID 冲突 | idPrefix | 前缀散落在调用点 | 在计划中集中校验 |
| 替代目录互相指向 | 替代链 | 没有环校验 | 构建前遍历并拒绝环 |
| 维护者误判配置已启用 | 计划覆盖 | 只阅读种子常量 | 以生成报告为运行事实 |
当前源码能够确认:SUBJECT_ONE_SEEDS 定义了十组种子;buildGeneratedTitle() 与 getGeneratedSeed() 有科一分支;buildBuiltinQuestions() 追加 42 道 SUBJECT_ONE_LIGHT_QUESTIONS,过滤掉 BASE_QUESTIONS 中的科一项,并且只为科二、科四调用自动生成函数。由此可以确认科一种子没有进入当前内置生成链。
GenerationPlan、目录注册表、生命周期状态、生成报告、替代转换和覆盖校验都是建议方案,尚未写入 The_kemusan。科一种子究竟应启用、被手工题替代,还是正式退役,需要内容与产品决策;文章不会替源码作者猜结论。这次没有运行构建,没有生成新的 HAP,也没有在模拟器或真机核对题库数量、搜索结果或旧引用迁移。
更多推荐


所有评论(0)