@autobest-ui/agent 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/bin/sync-assets.test.mjs +11 -0
  2. package/bin/traceability-validators.test.mjs +209 -0
  3. package/package.json +2 -2
  4. package/plugins/autobest-delivery/README.md +5 -1
  5. package/plugins/autobest-delivery/mcp-server/src/runner.mjs +48 -0
  6. package/plugins/autobest-delivery/mcp-server/tests/runner.test.mjs +98 -0
  7. package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -0
  8. package/plugins/autobest-delivery/skills/code-craft/SKILL.md +3 -3
  9. package/plugins/autobest-delivery/skills/delivery-loop/SKILL.md +4 -4
  10. package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +16 -1
  11. package/plugins/autobest-delivery/skills/e2e-gen-spec/SKILL.md +4 -0
  12. package/plugins/autobest-delivery/skills/e2e-ui-checker/SKILL.md +2 -0
  13. package/plugins/autobest-delivery/skills/export-report/SKILL.md +3 -0
  14. package/skills/README.md +5 -0
  15. package/skills/common/code-pr-submit/SKILL.md +110 -139
  16. package/skills/common/code-pr-submit/agents/openai.yaml +2 -2
  17. package/skills/common/make-spec/SKILL.md +37 -0
  18. package/skills/common/make-spec/agents/openai.yaml +4 -0
  19. package/skills/common/make-spec/references/spec-schema.md +82 -0
  20. package/skills/common/make-spec/scripts/validate-spec-traceability.mjs +130 -0
  21. package/skills/common/review-from-docs/SKILL.md +33 -0
  22. package/skills/common/review-from-docs/agents/openai.yaml +4 -0
  23. package/skills/common/review-from-docs/references/review-result-schema.md +42 -0
  24. package/skills/common/review-from-docs/scripts/validate-review-traceability.mjs +91 -0
  25. package/skills/common/ui-prd-scope/SKILL.md +6 -3
  26. package/skills/common/ui-prd-scope/references/requirement-traceability.md +82 -0
  27. package/skills/common/ui-prd-scope/references/scope-schema.md +28 -1
  28. package/skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs +104 -0
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: review-from-docs
3
+ description: 围绕已有页面 scope、PRD、RAG、Figma 和仓库证据逐条质询需求,保留稳定 REQ 编号并生成结构化 review-result。用户要求评审、质询或澄清 scope,或需要在生成 spec 前消除需求冲突时使用;不生成最终 spec 或业务代码。
4
+ ---
5
+
6
+ # 基于文档质询需求
7
+
8
+ 以用户指定的页面 `scope.md` 为入口,把每个需求的冲突、歧义和缺失决策质询到可生成规格的状态。开始前完整读取 [需求追踪契约](../ui-prd-scope/references/requirement-traceability.md) 和 [质询结果 Schema](references/review-result-schema.md)。
9
+
10
+ ## 输入
11
+
12
+ - 必需的页面 `scope.md`,以及其范围包根目录的 `scope-manifest.json`。
13
+ - scope 声明的 PRD 来源、RAG 引用、Figma JSON/PNG 和仓库事实。
14
+ - 可选的既有 `review/review-result.json`;继续质询时保留已确认结论。
15
+
16
+ 输入未声明 `traceabilitySchemaVersion: 1`、非全局 scope 缺少需求编号,或 scope 与 manifest 的编号不一致时停止并要求先用 `$ui-prd-scope` 刷新范围包。本 Skill 不创建、修改或重排需求编号。
17
+
18
+ ## 质询
19
+
20
+ 1. 建立 manifest 中全部需求的台账,保留 `active`、`deferred` 和 `removed`。逐条核对 scope 原文、PRD、RAG、Figma 与仓库事实;证据优先级沿用 scope,不把历史 RAG 或当前实现升级为新需求。
21
+ 2. 对每个 `active` 需求检查行为、状态转换、权限、空态/错误态、响应式、UI 状态、数据前置和可观察验收结果。已有证据能唯一回答时记录证据结论;需要业务取舍时向用户提问。
22
+ 3. 每次只提出一个相互关联的决策组,明确列出 `REQ-ID`、证据冲突、缺失信息和该回答会影响的验收边界。不得用默认值替用户消解业务冲突。
23
+ 4. 用户回答后立即更新内存台账;结论必须落到对应 `REQ-ID`。用户明确暂缓或取消时记录状态和原因,但不修改 scope manifest 的生命周期状态;将需要回写 scope 的差异列入 `scopeUpdatesRequired`。
24
+ 5. 所有问题处理完毕,按 Schema 写入并复读 `review/review-result.json` 和 `review/review-result.md`。两份产物结论一致,Markdown 使用简体中文,JSON 保持机器字段。
25
+ 6. 执行 `node .agents/skills/review-from-docs/scripts/validate-review-traceability.mjs <页面 scope 目录>`;根据实际安装位置调整脚本路径。校验失败时修正质询产物,不修改或放宽 scope 编号。
26
+
27
+ ## 完成条件
28
+
29
+ - Manifest 中每个需求在结果中恰好出现一次,没有未知编号。
30
+ - 每个 `active` 需求为 `confirmed` 或 `unresolved`;只有证据或用户回答足以确定验收边界时才能标为 `confirmed`。
31
+ - `confirmed` 需求至少包含一条 `decisions`,`unresolved` 需求至少包含一条 `remainingIssues`。
32
+ - 用户要求生成可交付 Spec 时,任何 `active` 需求仍为 `unresolved` 都是明确阻断,不得伪装为默认结论。
33
+ - 最终答复列出 confirmed/unresolved/deferred/removed 数量、产物路径和需要回写 scope 的生命周期变化;不生成 `spec.md`、不修改业务代码。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "需求文档质询"
3
+ short_description: "按需求编号质询 scope 中的冲突、歧义和缺失信息"
4
+ default_prompt: "使用 $review-from-docs 逐条质询当前 scope,并生成带需求编号的 review-result。"
@@ -0,0 +1,42 @@
1
+ # Review Result Schema
2
+
3
+ 质询结果写入页面 scope 目录下的 `review/review-result.json`:
4
+
5
+ ```json
6
+ {
7
+ "schemaVersion": 1,
8
+ "scope": {
9
+ "manifestPath": "../scope-manifest.json",
10
+ "directory": "01-product-detail",
11
+ "scopePath": "scope.md"
12
+ },
13
+ "requirements": [
14
+ {
15
+ "id": "REQ-PD-001",
16
+ "scopeStatus": "active",
17
+ "reviewStatus": "confirmed",
18
+ "questions": [
19
+ {
20
+ "question": "图片加载失败时是否保留 4:3 占位?",
21
+ "answer": "是,显示默认占位图",
22
+ "answeredBy": "user"
23
+ }
24
+ ],
25
+ "decisions": ["加载失败时保持 4:3 容器并显示默认占位图"],
26
+ "evidenceRefs": ["scope.md#REQ-PD-001", "mobile-default.json#12:34"],
27
+ "remainingIssues": []
28
+ }
29
+ ],
30
+ "scopeUpdatesRequired": []
31
+ }
32
+ ```
33
+
34
+ ## 约束
35
+
36
+ - `scope.manifestPath`、`scope.scopePath` 和所有本地产物路径相对于页面 scope 目录。
37
+ - `scopeStatus` 使用 `active`、`deferred`、`removed`。
38
+ - `reviewStatus` 使用 `confirmed`、`unresolved`、`deferred`、`removed`;后两者必须与 scope 状态相同。
39
+ - `answeredBy` 使用 `user` 或 `evidence`。证据回答必须在同一需求的 `evidenceRefs` 中可定位。
40
+ - `scopeUpdatesRequired` 只记录用户在质询中改变生命周期的需求,例如 `{ "id": "REQ-PD-003", "targetStatus": "deferred", "reason": "本迭代不实现" }`。它不是对 manifest 的静默修改授权。
41
+
42
+ `review-result.md` 按需求编号展示原始需求、证据、问题、回答、最终决策和未解决项,并在顶部汇总各状态数量。
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ const featureDir = path.resolve(process.argv[2] || '.');
7
+ const errors = [];
8
+ const requirementPattern = /^REQ-[A-Z][A-Z0-9]{1,15}-\d{3}$/;
9
+ const reviewStatuses = new Set(['confirmed', 'unresolved', 'deferred', 'removed']);
10
+ const fail = message => errors.push(message);
11
+
12
+ function readJson(filePath, label) {
13
+ if (!fs.existsSync(filePath)) {
14
+ fail(`缺少 ${label}:${filePath}`);
15
+ return null;
16
+ }
17
+ try {
18
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
19
+ } catch (error) {
20
+ fail(`${label} JSON 无效:${error.message}`);
21
+ return null;
22
+ }
23
+ }
24
+
25
+ const reviewPath = path.join(featureDir, 'review/review-result.json');
26
+ const review = readJson(reviewPath, 'review-result.json');
27
+
28
+ if (review) {
29
+ if (review.schemaVersion !== 1) fail('review-result.schemaVersion 必须为 1');
30
+ const manifestPath = path.resolve(featureDir, review.scope?.manifestPath || '');
31
+ const scopePath = path.resolve(featureDir, review.scope?.scopePath || '');
32
+ const manifest = readJson(manifestPath, 'scope-manifest.json');
33
+ if (!review.scope?.manifestPath) fail('review-result 缺少 scope.manifestPath');
34
+ if (!review.scope?.scopePath) fail('review-result 缺少 scope.scopePath');
35
+ if (!fs.existsSync(scopePath)) fail(`Scope 不存在:${review.scope?.scopePath}`);
36
+ if (manifest?.traceabilitySchemaVersion !== 1) {
37
+ fail('Manifest 未启用 traceabilitySchemaVersion: 1');
38
+ }
39
+ const manifestScope = manifest?.scopes?.find(
40
+ item => item.directory === review.scope?.directory
41
+ );
42
+ if (!manifestScope) fail(`Manifest 未找到 scope:${review.scope?.directory}`);
43
+ const expected = new Map(
44
+ (manifestScope?.requirements || []).map(item => [item.id, item])
45
+ );
46
+ const requirements = Array.isArray(review.requirements) ? review.requirements : [];
47
+ const ids = requirements.map(item => item?.id);
48
+ if (new Set(ids).size !== ids.length) fail('review-result 包含重复需求 ID');
49
+
50
+ for (const id of expected.keys()) {
51
+ if (!ids.includes(id)) fail(`review-result 缺少需求:${id}`);
52
+ }
53
+ for (const item of requirements) {
54
+ if (!requirementPattern.test(item?.id || '')) {
55
+ fail(`需求编号格式无效:${item?.id}`);
56
+ continue;
57
+ }
58
+ const source = expected.get(item.id);
59
+ if (!source) {
60
+ fail(`review-result 包含未知需求:${item.id}`);
61
+ continue;
62
+ }
63
+ if (item.scopeStatus !== source.status) {
64
+ fail(`${item.id} 的 scopeStatus 与 manifest 不一致`);
65
+ }
66
+ if (!reviewStatuses.has(item.reviewStatus)) {
67
+ fail(`${item.id} 的 reviewStatus 无效:${item.reviewStatus}`);
68
+ }
69
+ if (source.status === 'active' && !['confirmed', 'unresolved'].includes(item.reviewStatus)) {
70
+ fail(`${item.id} 是 active,reviewStatus 必须为 confirmed 或 unresolved`);
71
+ }
72
+ if (source.status !== 'active' && item.reviewStatus !== source.status) {
73
+ fail(`${item.id} 的排除状态必须保持为 ${source.status}`);
74
+ }
75
+ if (item.reviewStatus === 'confirmed' &&
76
+ (!Array.isArray(item.decisions) || item.decisions.length === 0)) {
77
+ fail(`${item.id} 已 confirmed 但缺少 decisions`);
78
+ }
79
+ if (item.reviewStatus === 'unresolved' &&
80
+ (!Array.isArray(item.remainingIssues) || item.remainingIssues.length === 0)) {
81
+ fail(`${item.id} 仍 unresolved 但缺少 remainingIssues`);
82
+ }
83
+ }
84
+ if (!Array.isArray(review.scopeUpdatesRequired)) {
85
+ fail('review-result.scopeUpdatesRequired 必须是数组');
86
+ }
87
+ }
88
+
89
+ for (const error of errors) console.error(`错误 ${error}`);
90
+ console.log(`Review 追踪校验:${errors.length} 个错误`);
91
+ process.exit(errors.length === 0 ? 0 : 1);
@@ -18,8 +18,10 @@ description: 通过 MCP 拉取 Azure DevOps PRD PR,按共享 UI 页面族拆
18
18
 
19
19
  1. 读取仓库 `AGENTS.md`,检查现有输出目录和 Figma 采集目录,保留无关文件及用户创建的文件。
20
20
  2. 使用完整 PR 链接调用 `mcp__azurepr_mcp_bridge__get_azure_pull_request`。记录 PR 元数据、所有变更 PRD 文件,以及 bridge 返回的是完整文档还是仅新增片段。
21
- 3. 写入前确定输出身份:
22
- - 创建 `scope-manifest.json`,记录 PR 链接/ID/path、RAG 平台、来源到 scope 的映射、页面族决策、每个 scope 的 RAG 覆盖、生成目录、未匹配来源和 Figma 溯源。
21
+ 3. 写入前完整读取 [references/requirement-traceability.md](references/requirement-traceability.md),确定输出身份和编号:
22
+ - 创建 `scope-manifest.json`,声明 `traceabilitySchemaVersion: 1`,记录 PR 链接/ID/path、RAG 平台、来源到 scope 的映射、页面族决策、每个 scope 的 RAG 覆盖、生成目录、未匹配来源和 Figma 溯源。
23
+ - 为每个非全局 scope 选择稳定的 2 至 16 位大写 ASCII 简称,并以 `REQ-<简称>-<三位序号>` 分配需求 ID。一个可独立确认、实现和验收的需求使用一个 ID。
24
+ - 刷新已有范围包时,先按业务含义和来源定位已有需求并复用 ID;调整顺序不改号,删除或暂缓保留原 ID 和原因,新增需求使用该 scope 从未使用过的下一序号。不得回收或静默重排编号。
23
25
  - 输出根目录已有相同 PR/path 的 manifest 时,只刷新该 manifest 所属文件。在 manifest 或报告中标记失效 scope;只有用户确认后才能删除。
24
26
  - 目录属于其他 PR,或包含没有身份信息的旧文件时,默认写入 `<output-root>/pr-<id>`;用户明确授权迁移时除外。不得静默混合两个 PR。
25
27
  4. 写入前建立来源清单。先分离全局规则和页面规则,再按共享 UI 模板分组,不能按文档或 URL 机械拆分。用户提供的页面族分类优先。否则依据布局/DOM、工作流/状态、模块所有权和数据契约记录合并或拆分理由。同一 UI 的不同 URL 作为变体;用户明确认定为一个页面族时,局部差异本身不构成拆分理由。
@@ -39,7 +41,7 @@ description: 通过 MCP 拉取 Azure DevOps PRD PR,按共享 UI 页面族拆
39
41
  - 将无歧义的匹配资产复制到页面目录,并在 manifest 记录原路径。只有用户明确要求时才移动源文件。未匹配资产保留原位。
40
42
  - 在 `scope.md` 中列出每一组本地 JSON/PNG;只总结设计证据直接支持的布局和状态,不转储原始 JSON。
41
43
  - 按 PRD 改动点标明每个资产覆盖的组件、设备和状态。只在改动点缺少必需视觉证据时记录待补采;局部组件改动不要求完整页面截图。不得把组件截图静默当作完整页面设计,也不得因它不是整页而把已覆盖的局部改动误报为缺失。
42
- 9. 创建或重写页面 scope 前,读取 [references/scope-schema.md](references/scope-schema.md)。把适用的全局规则展开到各页面文档中。每个页面 scope 都要重复 Azure PR 链接、证据类型和完整 RAG 溯源,不依赖 README、全局文件或研究笔记才能理解。RAG 事实放在它所补充的功能或状态附近并附引用;不要另建后续提示必须读取的 RAG 或问题文档。
44
+ 9. 创建或重写页面 scope 前,读取 [references/scope-schema.md](references/scope-schema.md)。把适用的全局规则展开到各页面文档中。每个页面 scope 都要包含与 manifest 一致的“需求清单”,重复 Azure PR 链接、证据类型和完整 RAG 溯源,不依赖 README、全局文件或研究笔记才能理解。RAG 事实放在它所补充的功能或状态附近并附引用;不要另建后续提示必须读取的 RAG 或问题文档。
43
45
  10. 按以下优先级处理证据:当前 PRD 明确业务规则、当前 Figma 视觉证据、RAG 历史 PRD、仓库当前实现。冲突保留在“待质询”;实现证据只描述当前约束,不能覆盖新需求。
44
46
  11. 执行校验:
45
47
 
@@ -56,6 +58,7 @@ node .agents/skills/ui-prd-scope/scripts/validate-scope-bundle.mjs <实际输出
56
58
  - 每个页面 scope 都包含需求来源、运行环境/模块、改动边界、URL、权限/前置条件、Figma 状态、带溯源的页面级 RAG 和待质询问题。
57
59
  - 每个 RAG 覆盖领域都标记为命中、未命中、不适用或由用户明确暂缓;隐含的未检索缺口视为错误。
58
60
  - 每个页面族的合并/拆分都有依据,或明确引用用户提供的分类。
61
+ - 每个非全局 scope 的简称和需求 ID 格式有效且全范围包唯一;manifest 与 `scope.md` 的编号、正文、状态和来源一致。
59
62
  - 每个本地资产链接有效,同目录 JSON 均可解析。
60
63
  - 缺少不可从仓库获得的路由/环境前置、业务数据、权限、改动点所需 Figma 状态或 RAG 证据时,明确由开发、产品、设计或后续质询补充。仓库可发现的运行事实由后续角色自行解析,不转嫁为用户实时输入。
61
64
  - 最终答复链接范围索引,列出页面族,汇总未命中和阻塞缺口,并说明未修改业务代码。
@@ -0,0 +1,82 @@
1
+ # 需求追踪契约
2
+
3
+ 本契约定义页面 scope 从 PRD 到交付证据使用的稳定需求编号。`ui-prd-scope` 创建并拥有编号;后续质询、规格、实现、验收、审计和 PR 只能引用,不得重编号。
4
+
5
+ ## 编号
6
+
7
+ 格式固定为:
8
+
9
+ ```text
10
+ REQ-<页面或Scope简称>-<三位序号>
11
+ ```
12
+
13
+ - 简称使用 2 至 16 位大写 ASCII 字母或数字,以字母开头,例如 `PD`、`PL`、`GLOBAL`。
14
+ - 完整编号匹配 `^REQ-[A-Z][A-Z0-9]{1,15}-\d{3}$`。
15
+ - 每个页面族在 `scope-manifest.json` 中声明唯一的 `requirementPrefix`。
16
+ - 序号从 `001` 递增。调整展示顺序时保留原编号;新增需求使用该前缀下从未使用过的下一个序号。
17
+ - 一个编号只描述一个可独立实现或验收的原子需求。背景、目标、会议记录和普通备注不编号。
18
+
19
+ ## 生命周期
20
+
21
+ 需求状态只使用:
22
+
23
+ | 状态 | 含义 |
24
+ | --- | --- |
25
+ | `active` | 本次必须进入质询、规格、实现和验收 |
26
+ | `deferred` | 用户明确暂缓,保留编号和原因 |
27
+ | `removed` | 用户明确取消,保留编号和原因 |
28
+
29
+ 刷新已有 scope 时先读取旧 manifest,按来源和语义复用原编号。需求被拆分时保留原编号并为新增原子需求分配新编号;需求合并展示时仍保留所有原编号。不得用内容哈希、表格行号、E2E scene、检查 ID 或 `reportGroup` 代替需求编号。
30
+
31
+ ## Manifest 结构
32
+
33
+ 声明 `traceabilitySchemaVersion: 1` 的新范围包必须在每个非全局 scope 中包含:
34
+
35
+ ```json
36
+ {
37
+ "directory": "01-product-detail",
38
+ "pageFamily": "product-detail",
39
+ "requirementPrefix": "PD",
40
+ "requirements": [
41
+ {
42
+ "id": "REQ-PD-001",
43
+ "statement": "商品主图容器保持 4:3 比例",
44
+ "status": "active",
45
+ "sourceRefs": [
46
+ {
47
+ "type": "azure-prd",
48
+ "path": "/Frontend_Web/product-detail.md",
49
+ "section": "商品图片"
50
+ }
51
+ ],
52
+ "figmaRefs": ["mobile-default.json#12:34"],
53
+ "ragRefs": ["doc_id=81"],
54
+ "statusReason": null
55
+ }
56
+ ]
57
+ }
58
+ ```
59
+
60
+ `sourceRefs` 至少包含一个一手 PRD 来源;RAG 和当前实现只能补充,不能成为新增需求的唯一来源。`deferred` 和 `removed` 必须提供非空 `statusReason`。
61
+
62
+ ## Scope 文档
63
+
64
+ 每个非全局 `scope.md` 在“改动范围”之前包含:
65
+
66
+ ```markdown
67
+ ## 需求清单
68
+
69
+ | 需求 ID | 原子需求 | 状态 | PRD 来源 | Figma | RAG |
70
+ | --- | --- | --- | --- | --- | --- |
71
+ | `REQ-PD-001` | 商品主图容器保持 4:3 比例 | `active` | `/Frontend_Web/product-detail.md` 商品图片 | `mobile-default.json#12:34` | `doc_id=81` |
72
+ ```
73
+
74
+ 正文中讨论需求、冲突和待质询项时引用同一编号。Manifest 是机器追踪源,`scope.md` 是自包含的人读入口,两者的编号、描述和状态必须一致。
75
+
76
+ ## 覆盖不变量
77
+
78
+ - 每个 `active` 需求必须在质询结果中出现且最终为 `confirmed`,之后才能生成可交付 Spec。
79
+ - 每个 `active` 需求必须在 `spec.md` 和 `spec-traceability.json` 中出现。
80
+ - 每个 Spec 需求必须由至少一个运行检查或视觉映射覆盖;同一需求可对应多个检查。
81
+ - Maker、Checker、Auditor 和 PR 描述只报告已有编号,不根据文本猜测或创建编号。
82
+ - `deferred` 和 `removed` 不进入实现与验收覆盖,但必须在质询和 Spec 的排除清单中保留。
@@ -24,6 +24,7 @@
24
24
  ```json
25
25
  {
26
26
  "schemaVersion": 1,
27
+ "traceabilitySchemaVersion": 1,
27
28
  "prId": "<Azure PR ID>",
28
29
  "prUrl": "<完整 PR 链接>",
29
30
  "requestedPath": "<PR path 过滤条件或 null>",
@@ -33,6 +34,7 @@
33
34
  {
34
35
  "directory": "01-page-family",
35
36
  "pageFamily": "<页面族>",
37
+ "requirementPrefix": "PF",
36
38
  "decision": "<用户分类或合并/拆分依据>",
37
39
  "sourceFiles": ["/Frontend_APP/..."],
38
40
  "ragCoverage": {
@@ -41,7 +43,24 @@
41
43
  "emptyErrors": "no-hit",
42
44
  "permissions": "no-hit",
43
45
  "crossPage": "not-applicable"
44
- }
46
+ },
47
+ "requirements": [
48
+ {
49
+ "id": "REQ-PF-001",
50
+ "statement": "<可独立确认、实现和验收的需求>",
51
+ "status": "active",
52
+ "sourceRefs": [
53
+ {
54
+ "type": "azure-prd",
55
+ "path": "/Frontend_APP/...",
56
+ "section": "<章节>"
57
+ }
58
+ ],
59
+ "figmaRefs": ["mobile-default.png#<节点或状态>"],
60
+ "ragRefs": ["<doc_id>#<规则>"],
61
+ "statusReason": null
62
+ }
63
+ ]
45
64
  }
46
65
  ],
47
66
  "unmappedSources": [],
@@ -57,6 +76,8 @@
57
76
  }
58
77
  ```
59
78
 
79
+ `traceabilitySchemaVersion: 1` 表示该范围包启用需求追踪。`00-global` 可不声明 `requirementPrefix` 和 `requirements`;其他 scope 必须声明。编号、生命周期和刷新规则以 [requirement-traceability.md](requirement-traceability.md) 为准。旧范围包未声明 `traceabilitySchemaVersion` 时仍按旧契约读取,但进入 `review-from-docs` 前必须由本 Skill 刷新。
80
+
60
81
  每条 Figma 溯源记录必须指向 manifest 中已有的 scope 和实际存在的目标目录。默认使用 `operation: "copy"`,并要求源文件继续存在。只有用户明确要求移动时才能使用 `operation: "move"`。`operation: "legacy-move"` 与 `sourceStatus: "not-present-after-move"` 只用于审计本技能采用默认复制策略之前已移动的历史资产。
61
82
 
62
83
  ## 页面 Scope 模板
@@ -94,6 +115,12 @@
94
115
 
95
116
  - <PRD 变更边界>
96
117
 
118
+ ## 需求清单
119
+
120
+ | 需求 ID | 状态 | 需求陈述 | PRD 来源 | Figma 证据 | RAG 证据 | 状态原因 |
121
+ | --- | --- | --- | --- | --- | --- | --- |
122
+ | `REQ-PF-001` | `active` | <可独立确认、实现和验收的需求> | `<路径>#<章节>` | `<文件>#<节点/状态>` 或 `无` | `<doc_id>#<规则>` 或 `无` | - |
123
+
97
124
  ## 范围外
98
125
 
99
126
  - <明确排除或暂缓的页面>
@@ -19,6 +19,8 @@ const coverageStatuses = new Set([
19
19
  'not-applicable',
20
20
  'deferred-by-user'
21
21
  ]);
22
+ const requirementIdPattern = /^REQ-([A-Z][A-Z0-9]{1,15})-\d{3}$/;
23
+ const requirementStatuses = new Set(['active', 'deferred', 'removed']);
22
24
 
23
25
  const fail = message => errors.push(message);
24
26
 
@@ -43,6 +45,12 @@ if (fs.existsSync(manifestPath)) {
43
45
  }
44
46
 
45
47
  if (manifest) {
48
+ if (
49
+ manifest.traceabilitySchemaVersion !== undefined &&
50
+ manifest.traceabilitySchemaVersion !== 1
51
+ ) {
52
+ fail('Manifest traceabilitySchemaVersion 仅支持 1');
53
+ }
46
54
  for (const field of [
47
55
  'schemaVersion',
48
56
  'prId',
@@ -92,6 +100,9 @@ const scopeDirs = fs
92
100
  if (scopeDirs.length === 0) fail('没有找到包含 scope.md 的页面目录');
93
101
 
94
102
  const manifestScopes = new Map();
103
+ const manifestRequirementIds = new Set();
104
+ const manifestRequirementPrefixes = new Set();
105
+ const traceabilityEnabled = manifest?.traceabilitySchemaVersion === 1;
95
106
  if (manifest && Array.isArray(manifest.scopes)) {
96
107
  for (const item of manifest.scopes) {
97
108
  if (!item || typeof item.directory !== 'string') {
@@ -123,6 +134,55 @@ if (manifest && Array.isArray(manifest.scopes)) {
123
134
  fail(`Manifest ${item.directory} 必须说明 RAG 暂缓原因`);
124
135
  }
125
136
  }
137
+ if (traceabilityEnabled && item.directory !== '00-global') {
138
+ if (!/^[A-Z][A-Z0-9]{1,15}$/.test(item.requirementPrefix || '')) {
139
+ fail(`Manifest ${item.directory} 的 requirementPrefix 格式无效`);
140
+ } else if (manifestRequirementPrefixes.has(item.requirementPrefix)) {
141
+ fail(`Manifest 包含重复 requirementPrefix:${item.requirementPrefix}`);
142
+ } else {
143
+ manifestRequirementPrefixes.add(item.requirementPrefix);
144
+ }
145
+ if (!Array.isArray(item.requirements) || item.requirements.length === 0) {
146
+ fail(`Manifest ${item.directory} 必须包含非空 requirements`);
147
+ } else {
148
+ for (const requirement of item.requirements) {
149
+ const id = requirement?.id;
150
+ const match = typeof id === 'string' ? id.match(requirementIdPattern) : null;
151
+ if (!match) {
152
+ fail(`Manifest ${item.directory} 包含无效需求 ID:${id}`);
153
+ continue;
154
+ }
155
+ if (match[1] !== item.requirementPrefix) {
156
+ fail(`Manifest ${item.directory} 的 ${id} 与 requirementPrefix 不一致`);
157
+ }
158
+ if (manifestRequirementIds.has(id)) {
159
+ fail(`Manifest 包含重复需求 ID:${id}`);
160
+ }
161
+ manifestRequirementIds.add(id);
162
+ if (!requirement.statement || typeof requirement.statement !== 'string') {
163
+ fail(`Manifest ${item.directory} 的 ${id} 缺少 statement`);
164
+ }
165
+ if (!requirementStatuses.has(requirement.status)) {
166
+ fail(`Manifest ${item.directory} 的 ${id} 状态无效:${requirement.status}`);
167
+ }
168
+ if (!Array.isArray(requirement.sourceRefs) || requirement.sourceRefs.length === 0) {
169
+ fail(`Manifest ${item.directory} 的 ${id} 缺少 sourceRefs`);
170
+ } else if (requirement.sourceRefs.some(ref =>
171
+ !ref || ref.type !== 'azure-prd' || typeof ref.path !== 'string' || !ref.path
172
+ )) {
173
+ fail(`Manifest ${item.directory} 的 ${id} 包含无效 sourceRefs`);
174
+ }
175
+ for (const field of ['figmaRefs', 'ragRefs']) {
176
+ if (!Array.isArray(requirement[field])) {
177
+ fail(`Manifest ${item.directory} 的 ${id} 缺少数组字段 ${field}`);
178
+ }
179
+ }
180
+ if (['deferred', 'removed'].includes(requirement.status) && !requirement.statusReason) {
181
+ fail(`Manifest ${item.directory} 的 ${id} 必须说明 statusReason`);
182
+ }
183
+ }
184
+ }
185
+ }
126
186
  }
127
187
 
128
188
  for (const dirName of scopeDirs) {
@@ -225,6 +285,50 @@ for (const dirName of scopeDirs) {
225
285
  if (!content.includes('合并依据:')) {
226
286
  fail(`${dirName}:缺少页面族合并依据`);
227
287
  }
288
+ if (traceabilityEnabled && !/^## 需求清单$/m.test(content)) {
289
+ fail(`${dirName}:缺少“需求清单”`);
290
+ }
291
+ }
292
+
293
+ if (traceabilityEnabled && !isGlobal) {
294
+ const traceableScope = manifestScopes.get(dirName);
295
+ const expectedIds = new Set((traceableScope?.requirements || []).map(item => item.id));
296
+ const allFoundIds = [...content.matchAll(/\bREQ-[A-Z][A-Z0-9]{1,15}-\d{3}\b/g)].map(
297
+ match => match[0]
298
+ );
299
+ const foundIdSet = new Set(allFoundIds);
300
+ const requirementHeading = '## 需求清单';
301
+ const requirementStart = content.indexOf(requirementHeading);
302
+ const requirementEnd = content.indexOf(
303
+ '\n## ',
304
+ requirementStart + requirementHeading.length
305
+ );
306
+ const requirementSection = requirementStart === -1
307
+ ? ''
308
+ : content.slice(
309
+ requirementStart + requirementHeading.length,
310
+ requirementEnd === -1 ? content.length : requirementEnd
311
+ );
312
+ const tableIds = [...requirementSection.matchAll(
313
+ /\bREQ-[A-Z][A-Z0-9]{1,15}-\d{3}\b/g
314
+ )].map(match => match[0]);
315
+ for (const id of expectedIds) {
316
+ if (!foundIdSet.has(id)) fail(`${dirName}:scope.md 缺少需求 ID ${id}`);
317
+ if (tableIds.filter(value => value === id).length !== 1) {
318
+ fail(`${dirName}:需求清单中的 ${id} 必须恰好出现一次`);
319
+ }
320
+ }
321
+ for (const id of foundIdSet) {
322
+ if (!expectedIds.has(id)) fail(`${dirName}:scope.md 包含 manifest 未声明的需求 ID ${id}`);
323
+ }
324
+ for (const requirement of traceableScope?.requirements || []) {
325
+ const row = requirementSection
326
+ .split('\n')
327
+ .find(line => line.includes(requirement.id));
328
+ if (row && !row.includes(`\`${requirement.status}\``)) {
329
+ fail(`${dirName}:需求清单中 ${requirement.id} 的状态与 manifest 不一致`);
330
+ }
331
+ }
228
332
  }
229
333
 
230
334
  for (const fields of [['Workspace', '工作区'], ['页面模块'], ['启动'], ['权限'], ['数据前置']]) {