testspec-publish:用例入库(知识库管理)
铁律:绝不能删除 TestLib 用例,也不能隐式覆盖不同 ID 的语义匹配项。同 ID 可更新;所有歧义匹配都必须由用户明确裁决。
TestSpec 发布进度:
- [ ] 步骤 1:定位当前 change 目录 ⚠️ 必需
- [ ] 步骤 2:加载 testcases 和可选 review/testpoints/proposal
- [ ] 步骤 3:评估路由与合并影响 ⚠️ 必需
- [ ] 步骤 4:写入 TestLib 前请求确认 ⛔ 阻塞
- [ ] 步骤 5:合并并重建 index、changelog、log 和统计信息
- [ ] 步骤 6:校验 TestLib 并报告变更文件
职责
将 testspec-review 评审通过的测试用例,从变更工作区(testspec/changes/<name>/)发布到持久化知识库(testspec/testlib/)。
核心能力:
- 自动分类:根据命名字典将用例路由到正确的
modules/<module>/<feature>.json - 增量合并:同 ID 受控更新、新 ID 新增、不主动删除已有用例
- 去重检测:入库前基于 Case ID 和用例标题识别疑似重复用例
- 交叉引用:自动推断功能间关系,更新受影响文件的
related_features - 全局索引:维护
index.json,为上游 skill 提供快速检索入口 - 操作日志:在
log.md顶部插入人可读记录(最新在前),维护changelog/结构化 JSON - 统计维护:更新
.testlib.json全局统计信息
当前变更目录
参见 ../_testspec-shared/references/common.md 的「当前变更目录定位规则」。
入库前评估
按 ../_testspec-shared/references/thinking-protocol.md 进行推理。以下是 publish 场景的核心评估问题:
Phase 1:材料评估
- canonical
artifacts/testcases.json是否存在且非空?正常流程不读取根目录副本 review-report.md是否存在?有无 S1 级阻塞问题?specs/testpoints.md中是否包含命名字典?testspec/testlib/中是否已有该模块/功能的用例?- 这次变更的用例是否适合长期沉淀?
- canonical source、testcases 和 review-report 的
source_revision是否一致? _context.origin/_context.trust是否表明这是未验证 Legacy Import?- context schema 是否为 v2,question graph 是否允许进入 publish,strategy requirement 是否满足?
Phase 2:策略推理
根据材料评估结果确定策略:
- 有命名字典 → 精确路由模式:MODULE/FEATURE 缩写决定目录和文件名
- 无命名字典 → 降级路由模式:
feature字段 kebab-case 作为路径,必须告知用户 - review-report 有 S1 问题 → 默认阻断;只有用户看到具体 S1 后明确要求 override 才能继续,并写入 changelog
- versioned workflow 版本不一致 → 无条件阻断;版本不一致不能通过 override 绕过
- context 缺少 v2/source_revision → 无条件阻断并提示先运行迁移器
- incoming provenance 缺失或无效 →
origin/trust非对象、关键枚举为空/未知、组合非法或 artifact/case 不一致时,归类为provenance-unknown并无条件阻断;不能使用 Legacy 确认绕过 - legacy-import + unverified → 无条件阻断;必须先关联当前 PRD/TP、重新生成并通过 review,不能用 override 绕过
- testlib 中已有同模块用例 → 进入 diff 合并,按 ID 匹配更新
Phase 3:执行决策
向用户简报(1-2 句话):
- 本次将发布 N 条用例到 M 个模块
- 其中预计新增 X 条,更新 Y 条
- 是否确认执行?(用户可中止)
输入策略
必需
| 文件 | 用途 |
|---|---|
artifacts/testcases.json |
canonical 用例数据源(对象格式,含 schema_version 和 testcases 数组) |
review-report.md |
versioned workflow 必需;证明当前 revision 已完成评审且无 unresolved S1 |
可选
| 文件 | 用途 |
|---|---|
proposal.md |
提取需求链接写入 changelog summary |
分类与路由规则
精确路由(有命名字典)
从 specs/testpoints.md 顶部的命名字典表格提取映射关系:
- 解析模块字典:
模块名称 → MODULE 缩写 - 解析功能点字典:
模块名称 + 功能点名称 → FEATURE 缩写 - 用例的
feature字段匹配模块名称 → 得到 MODULE - 用例标题的第二段匹配功能点名称 → 得到 FEATURE
- 目录转换:MODULE/FEATURE 大写转小写,
_转-
用例 feature="登录", 标题="登录_凭据验证_xxx"
→ MODULE=LOGIN → 目录 testlib/modules/login/
→ FEATURE=CRED → 文件 testlib/modules/login/cred.json
降级路由(无命名字典)
当 testpoints.md 不存在或缺少命名字典时:
- 用例的
feature字段作为模块名 - 用例标题的第二段(
_分割)作为功能名 - 中文转拼音或英文 kebab-case 作为目录/文件名
module_key和feature_key设为大写版本
必须告知用户正在使用降级路由,建议后续补充命名字典以确保一致性。
合并策略
写入前必须运行当前 skill 目录的确定性冲突检测器:
python "<testspec-publish-skill-dir>/scripts/detect_conflicts.py" \
--incoming <变更目录>/artifacts/testcases.json \
--testlib testspec/testlib \
--fail-on-conflict
<testspec-publish-skill-dir> 从当前已加载 SKILL.md 所在目录解析。退出码 2 表示存在 hard conflict,必须停止写入。
对每个目标 <feature>.json 文件执行增量合并:
| 情况 | 处理 |
|---|---|
| 目标文件不存在 | 创建新文件,所有用例 status = "active" |
| 文件存在,incoming 用例 ID 与库中匹配 | 受控更新:更新可变内容,保留 created_at、status,刷新 updated_at |
文件存在,ID 不匹配但 normalized title 相同或非空 scenario_key 相同 |
hard conflict,停止写入该条:使用检测器输出 existing/incoming ID、命中键和文件,请用户选择“新增独立用例”或“按已有 ID 更新” |
| 文件存在,ID 与语义均不匹配 | 新增:追加用例,created_at 和 updated_at 均为今天 |
| 库中有但本次 incoming 中未出现的 ID | 不动:不主动删除或变更状态 |
生命周期字段填充
| 字段 | 新入库 | 更新已有 |
|---|---|---|
| status | "active" |
保留原值(不自动变更) |
| source_change | 变更目录名 | 更新为当前变更目录名 |
| created_at | 今天 YYYY-MM-DD | 保留原值 |
| updated_at | 今天 YYYY-MM-DD | 今天 YYYY-MM-DD |
| tags | 空数组(或用户指定) | 合并原有 + 新增 |
执行步骤
1. 确定变更目录
按 ../_testspec-shared/references/common.md 定位规则确定 testspec/changes/<name>/。
2. 读取输入文件
- 读取
artifacts/testcases.json;缺失时终止并提示迁移或重新生成 - 读取
review-report.md(如存在,检查 S1 问题数量) - 读取
specs/testpoints.md(如存在,提取命名字典) - 读取
proposal.md(如存在,提取需求链接) - 读取 canonical source(优先
requirements.md,否则proposal.md)及三者 context
3. 入库前检查
- 用例源文件非空(
testcases数组长度 > 0) - canonical context 必须为 v2;无 review-report.md 时终止并提示先执行 testspec-review
- 运行 question validator
--target-stage publish;任何 blocker、隐藏 blocker 或环均无条件阻断 - versioned workflow 必须解析 review-report 末尾 context 的
review_gate:status = pass且s1_unresolved_count = 0→ 通过status = blocked、count > 0、s1_issue_ids非空或字段缺失 → 列出 issue IDs 并终止- 报告中保留的
resolved/accepted历史 S1 不计入 unresolved count - 用户在看到具体问题后明确要求 override 才可继续,并把 issue IDs、原因写入 changelog
_context.review_override
- 缺少
review_gate时阻断,不降级解析 Markdown - testcases 与 review-report 必须包含完全相同的 revision、questions 和 strategy_requirement;任一不一致都终止
- incoming
_context或任一 case 的origin/trust缺失、非对象、关键枚举为空/未知、组合非法或上下不一致时,标记为provenance-unknown并无条件阻断。空对象不算有效 provenance。必须先运行testspec-import,或从当前 PRD/TP 重新生成testspec-native + provisional用例;Legacy 告警确认和 review override 均不能绕过 _context.origin.kind = legacy-import且trust.status = unverified时,无论是否存在旧版 Markdown “通过”字样或用户要求 review override 都无条件阻断。必须先完成当前 PRD 对齐,重新经过 points/generate/review;publish 不能自动升级信任状态- 原生 versioned workflow 通过当前 revision review 后,publish 写入
origin.kind = testspec-native、trust.status = verified和trust.reviewed_revision
4–14. 按 TestLib 契约执行
写入前读取 references/testlib-contracts.md,严格执行其中的目录初始化、Schema、生命周期、交叉引用、索引、日志、统计和验证规则。主流程保持为:
- 读取现有 testlib,构建精确或降级路由;无法归类的用例进入
uncategorized/misc.json并告警。 - 运行
scripts/detect_conflicts.py,预检全库 ID、normalized title 和可选scenario_key:- 同 ID → 受控更新,保留
created_at与status。 - 不同 ID 且 normalized title 或非空
scenario_key相同 → hard conflict,等待用户选择新增或按已有 ID 更新。 - 无冲突的新 ID → 追加并填充生命周期字段。
- 仅靠模糊语义感觉相近、但未命中确定性键 → 可报告
possible_duplicate,绝不自动更新;不得把它伪装成 hard conflict。
- 同 ID → 受控更新,保留
- 仅在冲突全部解决且用户确认后写入 feature 文件。
- 累积更新双向
related_features,不自动删除历史引用。 - 重建
index.json,生成幂等 changelog,在log.md顶部插入记录并重算.testlib.json。 - 运行
validate_testlib.py;JSON、跨文件 ID、case_count、索引、引用、changelog 或 stats 不一致时最多自动修复 1 轮,仍失败则报告并停止。
15. 告知用户
输出发布摘要:
✅ 用例入库完成
变更:<change-name>
新增:X 条用例
更新:Y 条用例
涉及模块:<module-1>, <module-2>
新增交叉引用:N 条
文件变更:
- testspec/testlib/modules/<module>/<feature>.json(新增/更新)
- testspec/testlib/index.json(重建)
- testspec/testlib/log.md(顶部插入)
- testspec/testlib/changelog/<date>_<name>.json(新增)
建议提交:
git add testspec/testlib/
git commit -m "testlib: publish <change-name> 用例入库"
反模式识别
| 反模式 | 修正 |
|---|---|
| 用例未经当前 revision 评审直接入库 | 缺 review-report.md 或 review_gate 时阻断 |
| 所有增量用例都入库 | 提醒用户区分「资产型用例」和「任务型用例」,非所有变更都需要入库 |
| 入库后不提交 Git | 在摘要中明确提示 git commit 命令 |
| 同一 change 重复 publish 产生重复 | 幂等设计:同 ID 覆盖更新,changelog 同名覆盖 |
| 降级路由时不告知用户 | 必须明确提示正在使用降级路由及其影响 |
| 只入库不回顾 | 建议定期执行 testlib 健康检查(标记 stale 用例) |
| 旧 Excel/JSON 转换后直接入库 | 先运行 testspec-import;未验证导入默认阻断 |
| 无 provenance 的旧 JSON 作为 Legacy 直接入库 | 标记 provenance-unknown 并硬阻断;先隔离导入或从当前 PRD 重新生成 |
| 把 TestLib 当需求事实 | PRD-first;TestLib 仅用于回归、命名和风格 |
上下文传播
按 ../_testspec-shared/references/context-protocol.md,在 changelog 条目的 _context 中播种:
{
"_context": {
"source_skill": "testspec-publish",
"source_change": "<change-name>",
"publish_summary": {
"added": 5,
"updated": 2,
"deprecated": 0
},
"affected_modules": ["login", "order"],
"new_cross_refs": [
{ "from": "login/cred", "to": "register/basic", "relation": "前置依赖" }
]
}
}
下游 skill(如 testspec-analysis 检索已有用例时)可消费此元数据。
产物
| 产物 | 路径 | 说明 |
|---|---|---|
| 功能用例文件 | testspec/testlib/modules/<module>/<feature>.json |
新增或更新,含交叉引用 |
| 全局索引 | testspec/testlib/index.json |
每次 publish 后重建 |
| 操作日志 | testspec/testlib/log.md |
顶部插入新条目 |
| 变更日志 | testspec/testlib/changelog/<YYYY-MM-DD>_<change-name>.json |
结构化 JSON |
| 库统计 | testspec/testlib/.testlib.json |
更新 |
格式契约
testlib 知识库的详细 JSON Schema、字段说明和生命周期规则见 references/testlib-contracts.md。