@autobest-ui/agent 1.0.6 → 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 (27) 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 +7 -4
  16. package/skills/common/make-spec/SKILL.md +37 -0
  17. package/skills/common/make-spec/agents/openai.yaml +4 -0
  18. package/skills/common/make-spec/references/spec-schema.md +82 -0
  19. package/skills/common/make-spec/scripts/validate-spec-traceability.mjs +130 -0
  20. package/skills/common/review-from-docs/SKILL.md +33 -0
  21. package/skills/common/review-from-docs/agents/openai.yaml +4 -0
  22. package/skills/common/review-from-docs/references/review-result-schema.md +42 -0
  23. package/skills/common/review-from-docs/scripts/validate-review-traceability.mjs +91 -0
  24. package/skills/common/ui-prd-scope/SKILL.md +6 -3
  25. package/skills/common/ui-prd-scope/references/requirement-traceability.md +82 -0
  26. package/skills/common/ui-prd-scope/references/scope-schema.md +28 -1
  27. package/skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs +104 -0
@@ -22,12 +22,23 @@ test("installs common skills into the user-level skills directory", async () =>
22
22
  assert.deepEqual(result.skills, [
23
23
  "code-pr-submit",
24
24
  "figma-ui-capture",
25
+ "make-spec",
26
+ "review-from-docs",
25
27
  "ui-prd-scope",
26
28
  ]);
27
29
  await access(
28
30
  path.join(result.targetRoot, "code-pr-submit", "SKILL.md")
29
31
  );
30
32
  await access(path.join(result.targetRoot, "figma-ui-capture", "SKILL.md"));
33
+ await access(
34
+ path.join(result.targetRoot, "make-spec", "scripts", "validate-spec-traceability.mjs")
35
+ );
36
+ await access(
37
+ path.join(result.targetRoot, "review-from-docs", "references", "review-result-schema.md")
38
+ );
39
+ await access(
40
+ path.join(result.targetRoot, "review-from-docs", "scripts", "validate-review-traceability.mjs")
41
+ );
31
42
  await access(
32
43
  path.join(
33
44
  result.targetRoot,
@@ -0,0 +1,209 @@
1
+ import assert from 'node:assert/strict';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import test from 'node:test';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const scopeValidator = path.join(
12
+ packageRoot,
13
+ 'skills/common/ui-prd-scope/scripts/validate-scope-bundle.mjs'
14
+ );
15
+ const specValidator = path.join(
16
+ packageRoot,
17
+ 'skills/common/make-spec/scripts/validate-spec-traceability.mjs'
18
+ );
19
+ const reviewValidator = path.join(
20
+ packageRoot,
21
+ 'skills/common/review-from-docs/scripts/validate-review-traceability.mjs'
22
+ );
23
+
24
+ const hash = value => crypto.createHash('sha256').update(value).digest('hex');
25
+
26
+ async function createTraceableScope() {
27
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-traceability-'));
28
+ const pageDir = path.join(root, '01-product-detail');
29
+ await fs.mkdir(path.join(pageDir, 'review'), { recursive: true });
30
+ const scope = `# 商品详情 范围
31
+
32
+ ## 需求来源
33
+ - Azure PR: https://dev.azure.com/org/project/_git/repo/pullrequest/1
34
+ - 证据类型:\`PR 增量\`
35
+
36
+ ## 项目运行与访问
37
+ | 项目 | 内容 |
38
+ | --- | --- |
39
+ | 工作区 | \`packages/web\` |
40
+ | 页面模块 | \`src/detail\` |
41
+ | 启动 | \`npm start\` |
42
+ | 本地 URL | http://localhost:3000/detail |
43
+ | 权限 | 无登录门禁 |
44
+ | 数据前置 | fixture |
45
+
46
+ ## 页面族
47
+ 合并依据:共享页面模板。
48
+
49
+ ## 改动范围
50
+ - 商品主图。
51
+
52
+ ## 需求清单
53
+ | 需求 ID | 状态 | 需求陈述 | PRD 来源 | Figma 证据 | RAG 证据 | 状态原因 |
54
+ | --- | --- | --- | --- | --- | --- | --- |
55
+ | \`REQ-PD-001\` | \`active\` | 商品主图可见 | \`/Frontend_Web/detail.md#主图\` | 无 | \`doc_id=1\` | - |
56
+
57
+ ## 范围外
58
+ - 无。
59
+
60
+ ## Figma 输入
61
+ | 覆盖内容 | 设备 | 变体 | JSON | 截图 | 根节点/尺寸 |
62
+ | --- | --- | --- | --- | --- | --- |
63
+
64
+ ## RAG 补充
65
+ 来源:RAG \`web / detail / detail\`,\`详情需求\`(doc_id: \`1\`)。
66
+ | 查询重点 | 过滤条件 | 结果 |
67
+ | --- | --- | --- |
68
+ | 行为 | \`platform=web module=detail page=detail\` | doc_id=1 |
69
+ | 覆盖领域 | 状态 |
70
+ | --- | --- |
71
+ | businessRules | \`hit\` |
72
+ | statesTransitions | \`hit\` |
73
+ | emptyErrors | \`no-hit\` |
74
+ | permissions | \`no-hit\` |
75
+ | crossPage | \`not-applicable\` |
76
+
77
+ ## 待质询
78
+ - 无。
79
+ `;
80
+ const requirement = {
81
+ id: 'REQ-PD-001',
82
+ statement: '商品主图可见',
83
+ status: 'active',
84
+ sourceRefs: [{ type: 'azure-prd', path: '/Frontend_Web/detail.md', section: '主图' }],
85
+ figmaRefs: [],
86
+ ragRefs: ['doc_id=1'],
87
+ statusReason: null
88
+ };
89
+ const scopeEntry = {
90
+ directory: '01-product-detail',
91
+ pageFamily: '商品详情',
92
+ requirementPrefix: 'PD',
93
+ decision: '共享页面模板',
94
+ sourceFiles: ['/Frontend_Web/detail.md'],
95
+ ragCoverage: {
96
+ businessRules: 'hit',
97
+ statesTransitions: 'hit',
98
+ emptyErrors: 'no-hit',
99
+ permissions: 'no-hit',
100
+ crossPage: 'not-applicable'
101
+ },
102
+ requirements: [requirement]
103
+ };
104
+ const manifest = {
105
+ schemaVersion: 1,
106
+ traceabilitySchemaVersion: 1,
107
+ prId: '1',
108
+ prUrl: 'https://dev.azure.com/org/project/_git/repo/pullrequest/1',
109
+ requestedPath: null,
110
+ ragPlatform: 'web',
111
+ changedSources: ['/Frontend_Web/detail.md'],
112
+ scopes: [scopeEntry],
113
+ unmappedSources: [],
114
+ figmaSources: []
115
+ };
116
+ await fs.writeFile(path.join(root, 'README.md'), '[商品详情](./01-product-detail/scope.md)\n');
117
+ await fs.writeFile(path.join(root, 'scope-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
118
+ await fs.writeFile(path.join(pageDir, 'scope.md'), scope);
119
+ return { root, pageDir, scope, manifest };
120
+ }
121
+
122
+ function runValidator(script, target) {
123
+ return spawnSync(process.execPath, [script, target], { encoding: 'utf8' });
124
+ }
125
+
126
+ test('scope 校验器接受编号一致的范围包并拒绝重复 ID', async t => {
127
+ const fixture = await createTraceableScope();
128
+ t.after(() => fs.rm(fixture.root, { recursive: true, force: true }));
129
+ const valid = runValidator(scopeValidator, fixture.root);
130
+ assert.equal(valid.status, 0, valid.stderr);
131
+
132
+ fixture.manifest.scopes.push({
133
+ ...fixture.manifest.scopes[0],
134
+ directory: '02-duplicate',
135
+ pageFamily: '重复页面'
136
+ });
137
+ await fs.mkdir(path.join(fixture.root, '02-duplicate'));
138
+ await fs.copyFile(
139
+ path.join(fixture.pageDir, 'scope.md'),
140
+ path.join(fixture.root, '02-duplicate', 'scope.md')
141
+ );
142
+ await fs.appendFile(
143
+ path.join(fixture.root, 'README.md'),
144
+ '[重复页面](./02-duplicate/scope.md)\n'
145
+ );
146
+ await fs.writeFile(
147
+ path.join(fixture.root, 'scope-manifest.json'),
148
+ `${JSON.stringify(fixture.manifest, null, 2)}\n`
149
+ );
150
+ const invalid = runValidator(scopeValidator, fixture.root);
151
+ assert.equal(invalid.status, 1);
152
+ assert.match(invalid.stderr, /重复 requirementPrefix|重复需求 ID/);
153
+ });
154
+
155
+ test('spec 校验器验证完整映射和输入哈希', async t => {
156
+ const fixture = await createTraceableScope();
157
+ t.after(() => fs.rm(fixture.root, { recursive: true, force: true }));
158
+ const review = {
159
+ schemaVersion: 1,
160
+ scope: {
161
+ manifestPath: '../scope-manifest.json',
162
+ directory: '01-product-detail',
163
+ scopePath: 'scope.md'
164
+ },
165
+ requirements: [{
166
+ id: 'REQ-PD-001',
167
+ scopeStatus: 'active',
168
+ reviewStatus: 'confirmed',
169
+ decisions: ['商品主图必须可见'],
170
+ remainingIssues: []
171
+ }],
172
+ scopeUpdatesRequired: []
173
+ };
174
+ const spec = '# 商品详情功能规格\n\n## REQ-PD-001 商品主图\n\n### 验收条件\n\n- 商品主图可见。\n';
175
+ const reviewText = `${JSON.stringify(review, null, 2)}\n`;
176
+ await fs.writeFile(path.join(fixture.pageDir, 'review/review-result.json'), reviewText);
177
+ assert.equal(runValidator(reviewValidator, fixture.pageDir).status, 0);
178
+ await fs.writeFile(path.join(fixture.pageDir, 'spec.md'), spec);
179
+ const trace = {
180
+ schemaVersion: 1,
181
+ scope: {
182
+ manifestPath: '../scope-manifest.json',
183
+ directory: '01-product-detail',
184
+ scopePath: 'scope.md',
185
+ scopeSha256: hash(fixture.scope)
186
+ },
187
+ review: { path: 'review/review-result.json', sha256: hash(reviewText) },
188
+ spec: { path: 'spec.md', sha256: hash(spec) },
189
+ requirements: [{
190
+ id: 'REQ-PD-001',
191
+ status: 'specified',
192
+ heading: 'REQ-PD-001 商品主图',
193
+ acceptanceCriteria: ['商品主图可见'],
194
+ verification: ['runtime'],
195
+ figmaRefs: []
196
+ }],
197
+ excludedRequirements: []
198
+ };
199
+ await fs.writeFile(
200
+ path.join(fixture.pageDir, 'spec-traceability.json'),
201
+ `${JSON.stringify(trace, null, 2)}\n`
202
+ );
203
+ assert.equal(runValidator(specValidator, fixture.pageDir).status, 0);
204
+
205
+ await fs.appendFile(path.join(fixture.pageDir, 'spec.md'), '\n漂移\n');
206
+ const invalid = runValidator(specValidator, fixture.pageDir);
207
+ assert.equal(invalid.status, 1);
208
+ assert.match(invalid.stderr, /Spec SHA-256 与当前文件不一致/);
209
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobest-ui/agent",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "private": false,
5
5
  "description": "Autobest Agent skills/plugins/mcp assets + sync cli",
6
6
  "files": [
@@ -30,7 +30,7 @@
30
30
  "mcp:figma": "node ./mcp/figma-mcp-bridge/src/index.js",
31
31
  "mcp:rag": "node ./mcp/rag-mcp-bridge/index.js",
32
32
  "test": "npm run test:skills && npm run test:mcp && npm run test:plugin",
33
- "test:skills": "node --test ./bin/sync-assets.test.mjs",
33
+ "test:skills": "node --test ./bin/sync-assets.test.mjs ./bin/traceability-validators.test.mjs",
34
34
  "test:mcp:azurepr": "node --test ./mcp/azurepr-mcp-bridge/index.test.js",
35
35
  "test:mcp:code-mcp-pr": "node --test ./mcp/code-mcp-pr/index.test.js",
36
36
  "test:mcp:figma": "node --test ./mcp/figma-mcp-bridge/index.test.js",
@@ -90,6 +90,8 @@ npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup --help
90
90
 
91
91
  ```js
92
92
  export const metadata = {
93
+ traceabilitySchemaVersion: 1,
94
+ requirementIds: ['REQ-PD-001'],
93
95
  scenes: ['initial'],
94
96
  viewports: [{ name: 'desktop', width: 1400, height: 1000 }],
95
97
  visualMappings: []
@@ -125,7 +127,9 @@ Checker 和代码审计生成的 Markdown 使用简体中文,包括标题、
125
127
 
126
128
  ## 交付流程
127
129
 
128
- 1. `$delivery-loop` 调用 `check_environment` 并记录固定审查点。
130
+ 功能目录存在 `spec-traceability.json` 时,Delivery 按 active REQ ID 运行。Maker 报告实现编号,E2E 检查和视觉映射携带 `requirementId`,Checker 与 Audit 逐项核对;运行器拒绝格式错误、未知编号和无验收覆盖的需求。没有追踪文件的历史交付仍按旧契约运行。
131
+
132
+ 1. `$delivery-loop` 校验可用的需求追踪文件,调用 `check_environment` 并记录固定审查点。
129
133
  2. `$code-craft` 实现规格。
130
134
  3. `$e2e-gen-spec` 创建无依赖场景,运行冻结前 preflight,确认所有 scene 和检查可达后冻结。
131
135
  4. `$e2e-ui-checker` 启动声明的应用服务,调用 `run_feature_e2e`,并审查已映射的组件截图。
@@ -7,6 +7,7 @@ import { resolveArtifactPath, resolveWorkspacePaths } from './paths.mjs';
7
7
  const serverRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
8
  const defaultBrowserRoot = path.join(serverRoot, '.runtime', 'ms-playwright');
9
9
  const blockedSessions = new Map();
10
+ const requirementIdPattern = /^REQ-[A-Z][A-Z0-9]{1,15}-\d{3}$/;
10
11
  process.env.PLAYWRIGHT_BROWSERS_PATH =
11
12
  process.env.AUTOBEST_DELIVERY_BROWSERS_PATH || defaultBrowserRoot;
12
13
 
@@ -184,6 +185,8 @@ export async function runFeatureE2E({
184
185
  let traceStarted = false;
185
186
  let traceError;
186
187
  let result;
188
+ let traceabilityEnabled = false;
189
+ let requirementIds = new Set();
187
190
 
188
191
  try {
189
192
  const { chromium, expect } = await loadPlaywright();
@@ -195,9 +198,36 @@ export async function runFeatureE2E({
195
198
  }
196
199
  metadata = scenarioModule.metadata || {};
197
200
  const reportSchemaVersion = metadata.reportSchemaVersion;
201
+ const traceabilitySchemaVersion = metadata.traceabilitySchemaVersion;
198
202
  if (reportSchemaVersion !== undefined && reportSchemaVersion !== 1) {
199
203
  throw new TypeError('metadata.reportSchemaVersion 仅支持 1');
200
204
  }
205
+ if (traceabilitySchemaVersion !== undefined && traceabilitySchemaVersion !== 1) {
206
+ throw new TypeError('metadata.traceabilitySchemaVersion 仅支持 1');
207
+ }
208
+ traceabilityEnabled = traceabilitySchemaVersion === 1;
209
+ if (traceabilityEnabled) {
210
+ if (!Array.isArray(metadata.requirementIds) || metadata.requirementIds.length === 0) {
211
+ throw new TypeError('metadata.requirementIds 必须是非空数组');
212
+ }
213
+ requirementIds = new Set(metadata.requirementIds);
214
+ if (requirementIds.size !== metadata.requirementIds.length) {
215
+ throw new TypeError('metadata.requirementIds 不得包含重复项');
216
+ }
217
+ for (const id of requirementIds) {
218
+ if (typeof id !== 'string' || !requirementIdPattern.test(id)) {
219
+ throw new TypeError(`metadata.requirementIds 包含无效需求 ID:${id}`);
220
+ }
221
+ }
222
+ for (const mapping of metadata.visualMappings || []) {
223
+ if (!mapping || typeof mapping.requirementId !== 'string') {
224
+ throw new TypeError('visualMapping 必须包含 requirementId');
225
+ }
226
+ if (!requirementIds.has(mapping.requirementId)) {
227
+ throw new TypeError(`visualMapping 引用了未知需求 ID:${mapping.requirementId}`);
228
+ }
229
+ }
230
+ }
201
231
  if (reportSchemaVersion === 1) {
202
232
  for (const mapping of metadata.visualMappings || []) {
203
233
  for (const field of ['reportModule', 'reportGroup', 'reportTitle', 'reportDevice']) {
@@ -248,6 +278,14 @@ export async function runFeatureE2E({
248
278
  if (reportSchemaVersion === 1 && !/^[a-z0-9][a-z0-9._-]*$/.test(definition.reportGroup)) {
249
279
  throw new TypeError('check.reportGroup 必须是稳定的 ASCII ID');
250
280
  }
281
+ if (traceabilityEnabled) {
282
+ if (!definition || typeof definition.requirementId !== 'string') {
283
+ throw new TypeError('check 定义必须包含 requirementId');
284
+ }
285
+ if (!requirementIds.has(definition.requirementId)) {
286
+ throw new TypeError(`check 引用了未知需求 ID:${definition.requirementId}`);
287
+ }
288
+ }
251
289
  try {
252
290
  const actual = await assertion();
253
291
  checks.push({ ...definition, actual: actual ?? '符合预期', isPass: true });
@@ -351,6 +389,16 @@ export async function runFeatureE2E({
351
389
  }
352
390
  }
353
391
  }
392
+ if (traceabilityEnabled && scenarioBlockers.length === 0) {
393
+ const coveredRequirementIds = new Set([
394
+ ...checks.map(item => item.requirementId),
395
+ ...(metadata.visualMappings || []).map(item => item.requirementId)
396
+ ]);
397
+ const uncovered = [...requirementIds].filter(id => !coveredRequirementIds.has(id));
398
+ if (uncovered.length > 0) {
399
+ throw new TypeError(`以下需求 ID 没有运行或视觉覆盖:${uncovered.join(', ')}`);
400
+ }
401
+ }
354
402
  if (blockedPage && !blockedPage.isClosed()) {
355
403
  await blockedPage.bringToFront().catch(() => undefined);
356
404
  page = blockedPage;
@@ -249,6 +249,104 @@ test('runFeatureE2E 将无效场景语法报告为 blocked', async t => {
249
249
  assert.equal(result.blockers[0].kind, 'scenario');
250
250
  });
251
251
 
252
+ test('runFeatureE2E 接受具有完整需求覆盖的追踪场景', async t => {
253
+ const workspace = await createWorkspace();
254
+ t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
255
+ await fs.writeFile(workspace.scenarioPath, `export const metadata = {
256
+ traceabilitySchemaVersion: 1,
257
+ requirementIds: ['REQ-PD-001'],
258
+ visualMappings: []
259
+ };
260
+ export default async function ({ check }) {
261
+ await check({
262
+ id: 'tracked', requirementId: 'REQ-PD-001', specSnippet: '需求可验证。',
263
+ scene: 'initial', errorType: '功能缺陷', expect: '通过'
264
+ }, async () => '通过');
265
+ }\n`, 'utf8');
266
+
267
+ const result = await runFeatureE2E({
268
+ workspaceRoot: workspace.root,
269
+ scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
270
+ baseUrl: 'http://127.0.0.1:1',
271
+ outputDir: 'feature/e2e/runs/traceable-valid',
272
+ timeoutMs: 10000
273
+ });
274
+
275
+ assert.equal(result.status, 'passed');
276
+ assert.equal(result.checks[0].requirementId, 'REQ-PD-001');
277
+ });
278
+
279
+ for (const testCase of [
280
+ {
281
+ name: '缺少 check.requirementId',
282
+ requirementIds: "['REQ-PD-001']",
283
+ checkRequirement: '',
284
+ expected: /必须包含 requirementId/
285
+ },
286
+ {
287
+ name: '引用未知需求 ID',
288
+ requirementIds: "['REQ-PD-001']",
289
+ checkRequirement: "requirementId: 'REQ-PD-002',",
290
+ expected: /未知需求 ID/
291
+ },
292
+ {
293
+ name: '需求 ID 格式错误',
294
+ requirementIds: "['REQ-pd-1']",
295
+ checkRequirement: "requirementId: 'REQ-pd-1',",
296
+ expected: /无效需求 ID/
297
+ }
298
+ ]) {
299
+ test(`runFeatureE2E 拒绝${testCase.name}`, async t => {
300
+ const workspace = await createWorkspace();
301
+ t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
302
+ await fs.writeFile(workspace.scenarioPath, `export const metadata = {
303
+ traceabilitySchemaVersion: 1,
304
+ requirementIds: ${testCase.requirementIds},
305
+ visualMappings: []
306
+ };
307
+ export default async function ({ check }) {
308
+ await check({ id: 'tracked', ${testCase.checkRequirement} specSnippet: '需求可验证。',
309
+ scene: 'initial', errorType: '功能缺陷', expect: '通过' }, async () => '通过');
310
+ }\n`, 'utf8');
311
+
312
+ const result = await runFeatureE2E({
313
+ workspaceRoot: workspace.root,
314
+ scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
315
+ baseUrl: 'http://127.0.0.1:1',
316
+ outputDir: `feature/e2e/runs/traceable-${testCase.name}`,
317
+ timeoutMs: 10000
318
+ });
319
+
320
+ assert.equal(result.status, 'blocked');
321
+ assert.match(result.blockers[0].detail, testCase.expected);
322
+ });
323
+ }
324
+
325
+ test('runFeatureE2E 拒绝没有检查或视觉覆盖的已声明需求', async t => {
326
+ const workspace = await createWorkspace();
327
+ t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
328
+ await fs.writeFile(workspace.scenarioPath, `export const metadata = {
329
+ traceabilitySchemaVersion: 1,
330
+ requirementIds: ['REQ-PD-001', 'REQ-PD-002'],
331
+ visualMappings: []
332
+ };
333
+ export default async function ({ check }) {
334
+ await check({ id: 'tracked', requirementId: 'REQ-PD-001', specSnippet: '需求可验证。',
335
+ scene: 'initial', errorType: '功能缺陷', expect: '通过' }, async () => '通过');
336
+ }\n`, 'utf8');
337
+
338
+ const result = await runFeatureE2E({
339
+ workspaceRoot: workspace.root,
340
+ scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
341
+ baseUrl: 'http://127.0.0.1:1',
342
+ outputDir: 'feature/e2e/runs/traceable-uncovered',
343
+ timeoutMs: 10000
344
+ });
345
+
346
+ assert.equal(result.status, 'blocked');
347
+ assert.match(result.blockers[0].detail, /REQ-PD-002/);
348
+ });
349
+
252
350
  test('runFeatureE2E 拒绝包导入和 Node 全局变量', async t => {
253
351
  const workspace = await createWorkspace();
254
352
  t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
@@ -11,10 +11,13 @@ description: 在独立验收者通过后,从固定审查点检查交付改动
11
11
 
12
12
  - 可解析的固定审查点。
13
13
  - 功能规格及同目录的 scope。
14
+ - 同目录存在时的 `spec-traceability.json`,以及 Maker、Checker 的逐项需求结果。
14
15
  - 默认审查 `worktree`,也可审查用户明确指定的 `committed` 改动。
15
16
 
16
17
  ## 流程
17
18
 
19
+ 追踪模式下,Spec 审查轴必须逐个核对 active REQ ID 是否同时出现在 spec、Maker 结论、相关代码和 Checker 证据中;不得用整体通过代替单项覆盖,也不得接受未知或重编号的需求。
20
+
18
21
  1. 只冻结一次审查范围。审查工作树时纳入与任务相关的暂存、未暂存及新增文件,保留并排除无关的既有改动。
19
22
  2. 阅读适用的仓库规则、spec、scope、提交列表、改动文件和完整 diff。
20
23
  3. 并行启动两个独立审查者:Standards 轴检查已记录的规范、兼容性、缺陷和重大风险;Spec 轴检查遗漏、错误行为和未经支持的范围扩张。两者都必须引用文件位置和约束依据。
@@ -9,7 +9,7 @@ description: 根据功能 spec 和同目录 scope 实现交付需求,或修复
9
9
 
10
10
  ## 输入
11
11
 
12
- - 必需的功能 spec 和同目录 scope。
12
+ - 必需的功能 spec 和同目录 scope;存在时必须读取并校验 `spec-traceability.json`。
13
13
  - 仅在修复回合读取:最新验收者 `failed` 结果、测试编写者的 `implementation` 阻断项,或代码审计阻断结果。
14
14
 
15
15
  输入缺失或冲突,或者工作需要扩大用户授权范围时,必须按事实返回 `blocked`。
@@ -17,10 +17,10 @@ description: 根据功能 spec 和同目录 scope 实现交付需求,或修复
17
17
  ## 流程
18
18
 
19
19
  1. 阅读仓库规则、spec、scope、目标模块、相邻实现,以及仅与改动 UI 映射的视觉基准。修复回合中,把每个失败项追溯到业务代码中的原因。
20
- 2. 实现 spec,或修复已提供失败项的直接原因。保留已经通过的行为和用户既有改动。涉及 React 或 CSS 布局时使用 `$ui-structure-guard`。
20
+ 2. 按 active REQ ID 实现 spec,或按失败项携带的 REQ ID 修复直接原因。不得创建或重编号;发现无法映射的改动或失败项时阻断并指出编号缺口。保留已经通过的行为和用户既有改动。涉及 React 或 CSS 布局时使用 `$ui-structure-guard`。
21
21
  3. 执行仓库最小充分的 lint、类型检查、构建和现有测试反馈。仓库没有测试框架时不得自行引入。
22
22
  4. 可通过隔离运行器执行冻结场景以获得开发反馈,但实现者不得宣称验收通过,也不得修改冻结输入或其他角色的报告。
23
- 5. 按共享契约写入并复读 `featureDir/delivery/code-craft-result.json`。
23
+ 5. 按共享契约写入并复读 `featureDir/delivery/code-craft-result.json`。追踪模式下逐项填写 `implementedRequirements` 和 `blockedRequirements`,不得包含未知或重复 ID。
24
24
 
25
25
  ## 边界
26
26
 
@@ -9,7 +9,7 @@ description: 围绕功能 spec 调度相互隔离的实现者、E2E 测试编写
9
9
 
10
10
  ## 输入
11
11
 
12
- - 必需的功能 `spec.md`,并定位同目录的 `scope.md`。
12
+ - 必需的功能 `spec.md`,并定位同目录的 `scope.md`;存在时必须读取 `spec-traceability.json`。
13
13
  - 可选的实现者最大回合数,默认 `3`,包含首次实现。
14
14
  - 可选的审计固定点,默认为启动时的 `HEAD`。
15
15
 
@@ -21,11 +21,11 @@ description: 围绕功能 spec 调度相互隔离的实现者、E2E 测试编写
21
21
 
22
22
  ## 流程
23
23
 
24
- 1. **预检:**阅读 spec、scope、仓库规则、视觉映射、固定审查点和当前工作树。在启动实现者前调用 `autobest-delivery` MCP 的 `check_environment` 工具。若环境未就绪,保留工具证据、保持实现者回合数为零并进入 `awaiting-decision`,由用户选择重试、跳过环境验收并记录 waiver,或停止。按契约自行解析可发现的 URL、命令、路由变体、数据准备和定位器,不为这些可发现信息询问用户。
24
+ 1. **预检:**阅读 spec、scope、仓库规则、视觉映射、固定审查点和当前工作树。存在 `spec-traceability.json` 时先校验输入哈希、active ID 集合和规格章节;校验失败即阻断,不得启动实现者。在启动实现者前调用 `autobest-delivery` MCP 的 `check_environment` 工具。若环境未就绪,保留工具证据、保持实现者回合数为零并进入 `awaiting-decision`,由用户选择重试、跳过环境验收并记录 waiver,或停止。按契约自行解析可发现的 URL、命令、路由变体、数据准备和定位器,不为这些可发现信息询问用户。
25
25
  2. **状态:**将 `delivery/delivery-state.json` 写为 `running`,记录当前阶段、回合数、固定审查点、既有改动和当前产物。每次状态流转都更新该文件。
26
- 3. **实现者:**启动全新的 `$code-craft` 子代理。首回合只传入 spec 和 scope;修复回合只额外传入最新验收失败结果、实现阻断项或代码审计阻断结果。启动时增加实现者回合计数。
26
+ 3. **实现者:**启动全新的 `$code-craft` 子代理。首回合传入 spec、scope 和已有的 traceability;修复回合只额外传入最新验收失败结果、实现阻断项或代码审计阻断结果。启动时增加实现者回合计数。
27
27
  4. **测试编写者:**缺少 `e2e/e2e.feature.mjs` 时,启动全新的 `$e2e-gen-spec` 子代理。旧 `.spec.ts` 不是标准输入,应保持不变。测试编写者必须使用隔离 scene,并在冻结前通过同一运行器 preflight;场景代码、定位器、弹窗、fixture 或等待条件造成的 preflight Blocked 由测试编写者在草稿阶段自行修正并重跑。报告 `ready` 后才冻结 `.mjs`。环境、spec 或实现阻断进入 `awaiting-decision`,由用户选择补充输入、重试、分类、记录 waiver 或停止。
28
- 5. **验收者:**启动全新的 `$e2e-ui-checker` 子代理,并传入 spec、scope、冻结场景和迭代编号。验收者必须通过隔离 MCP 运行器以 headed 模式执行全部场景,然后只审查 spec 已映射的视觉基准。`passed` 时继续;`failed` 且仍有回合时返回实现者。
28
+ 5. **验收者:**启动全新的 `$e2e-ui-checker` 子代理,并传入 spec、scope、traceability、冻结场景和迭代编号。验收者必须通过隔离 MCP 运行器以 headed 模式执行全部场景,然后只审查 spec 已映射的视觉基准。`passed` 时继续;`failed` 且仍有回合时返回实现者。
29
29
  6. **Blocked 决策:**验收者返回 `awaiting-decision` 时,先确认运行器已在阻断 scene 后继续尝试其余独立 scene,再将交付状态写为同名状态,记录该验收代理,保留可见浏览器与证据,并把契约定义的六个决策及其 effect 交给用户。必须明确 `skip` 是接受阻断 scene 的缺失证据并继续审计,不是从异常语句下一行恢复。收到选择后,把决议回传给同一个验收代理,由它调用 `resolve_blocked_run`;验证 `human-decision.json` 后再按其 `effect` 继续。不得自行修改冻结场景、替用户接受风险或把 Blocked 当作终止;只有 `stop` 是终止决策。
30
30
  7. **代码审计:**验收通过,或用户 `accept`/`skip` 形成 waiver 后,从固定审查点启动全新的 `$code-audit` 子代理。存在阻断项且仍有回合时返回实现者,修复后重新执行完整验收。没有阻断项时完成交付。
31
31
  8. **完成:**标准场景、最新验收、最新代码审计、人工决议、状态与产物必须一致且不存在未授权提交。完整验收通过写入 `passed`;存在 `accept`/`skip` waiver 且审计通过写入 `passed-with-waivers`;只有用户明确选择 `stop` 才写入终止态 `blocked`。Maker 回合耗尽时先进入 `awaiting-decision`,不得替用户停止。
@@ -7,6 +7,7 @@
7
7
  | 产物 | 所有者 | 其他角色权限 |
8
8
  | --- | --- | --- |
9
9
  | `spec.md` | 产品输入 | 只读 |
10
+ | `spec-traceability.json` | 规格输入 | 只读 |
10
11
  | `scope.md` | 范围输入 | 只读 |
11
12
  | 已映射视觉基准 | 产品或设计输入 | 只读 |
12
13
  | 业务源码 | 实现者 | 测试编写者、验收者和代码审计者只读 |
@@ -21,6 +22,12 @@
21
22
 
22
23
  旧 `e2e.feature.spec.ts` 文件不是本契约的可执行输入。保持这些文件不变;缺少标准 `.mjs` 场景时生成新场景。
23
24
 
25
+ ## 需求追踪
26
+
27
+ 当功能目录包含 `spec-traceability.json` 时,交付必须按其中的 active REQ ID 运行,编号格式为 `REQ-<页面或Scope简称>-<三位序号>`。编号由上游 `ui-prd-scope` 所有;本插件的编排者、Maker、测试编写者、Checker 和 Audit 只能引用,不能创建、修改或重排。开始 Maker 前必须确认 scope、review、spec 及其 SHA-256 一致。
28
+
29
+ 每个 active REQ ID 必须同时具备规格章节、Maker 实现结论,以及至少一个运行检查或视觉映射;适用视觉验收的需求必须同时有视觉映射。`deferred` 和 `removed` 需求保留编号但不进入实现覆盖。任一 active 需求缺失、出现未知编号或证据不足时不得报告完整通过。旧功能目录没有 `spec-traceability.json` 时保持历史流程,不临时生成编号。
30
+
24
31
  ## 人读输出语言
25
32
 
26
33
  角色自行撰写的 Markdown、结论、摘要、检查说明、期望、实际结果、阻断原因和审计 finding 一律使用简体中文。`checker-result.md`、`code-audit-result.md` 等人读报告不得使用英文标题、英文叙述或 `Passed` / `Failed` 等英文展示状态。
@@ -81,6 +88,8 @@ JSON 字段名、状态枚举、检查 ID、scene、`reportGroup`、文件路径
81
88
  ```js
82
89
  export const metadata = {
83
90
  reportSchemaVersion: 1,
91
+ traceabilitySchemaVersion: 1,
92
+ requirementIds: ['REQ-PD-001'],
84
93
  scenes: ['initial'],
85
94
  viewports: [{ name: 'desktop', width: 1400, height: 1000 }],
86
95
  visualMappings: []
@@ -106,6 +115,8 @@ export default async function run({
106
115
 
107
116
  新建或修订场景必须声明 `metadata.reportSchemaVersion: 1`;运行器据此校验通用报告分组字段,未声明版本的历史冻结场景继续兼容。每条原子运行预期通过 `check(definition, assertion)` 记录。新建或修订的检查定义必须同时包含 `reportModule`、`reportGroup`、`reportTitle` 和 `reportMethod`:`reportModule` 是人读的页面或模块名称,`reportGroup` 是当前功能目录内稳定且唯一的功能组 ID,`reportTitle` 是该组在报告中的简短中文标题,`reportMethod` 是该组件或完整功能合并后的中文检查方式。属于同一 UI 组件、同一页面位置或同一完整用户操作流的文案、样式、布局、响应式行为和相关功能检查共用同一组及同一套模块、标题和检查方式;不同组件、独立业务能力、状态转换或风险边界不得为了减少行数而合并。分组只表达产品功能语义,不表达 E2E scene 或执行顺序。可能失败的用户操作和等待应放在所属的 `check` 或 `scene` 内,不能作为未归属的顶层 await。
108
117
 
118
+ 存在追踪输入的新场景必须声明 `metadata.traceabilitySchemaVersion: 1` 和完整、唯一的 `metadata.requirementIds`。每个 `check` 和 `visualMappings` 项必须包含一个已声明的 `requirementId`;运行器拒绝格式错误、未知编号和完全没有运行或视觉覆盖的声明编号。`reportGroup` 用于报告聚合,不能替代 REQ ID。
119
+
109
120
  spec 声明的每个视觉对比都必须在 `metadata.visualMappings` 中出现且只出现一次,并包含检查定义、场景、截图文件名、定位器说明、仓库相对路径基准、`reportDevice`(`desktop` 或 `mobile`)以及同样的 `reportModule`、`reportGroup`、`reportTitle`。视觉映射与它证明的运行检查必须复用同一功能组。运行检查与视觉映射必须完整且不重复地划分所有原子 spec。
110
121
 
111
122
  旧版直接使用顶层 `page` 的默认函数仍可执行,但它不具备 scene 级故障隔离;测试编写者创建或修订场景时必须使用 `scene()`。
@@ -176,6 +187,8 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
176
187
  "status": "implemented",
177
188
  "iteration": 1,
178
189
  "modifiedFiles": [],
190
+ "implementedRequirements": ["REQ-PD-001"],
191
+ "blockedRequirements": [],
179
192
  "verification": [],
180
193
  "blockers": []
181
194
  }
@@ -209,12 +222,14 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
209
222
 
210
223
  运行器 Blocked 时,验收者使用 `status: "awaiting-decision"`,并在结果中记录 `blockedSessionId`、过期时间、runner 证据和阻断项;验收者不得代替用户选择决议。
211
224
 
212
- 每个验收检查项包含 `id`、`specSnippet`、`scene`、`reportModule`、`reportGroup`、`reportTitle`、`reportMethod`、`errorType`(`功能缺陷` 或 `UI视觉缺陷`)、`expect`、`actual`、可选的 `evidencePath` 和 `isPass`。Checker 合并 Runner 检查时必须原样保留四个报告字段;同一 `reportGroup` 的 `reportModule`、`reportTitle` 和 `reportMethod` 必须一致。每条原子 spec 检查只出现一次。证据不完整时验收者状态为 `awaiting-decision`;证据完整但存在失败检查时为 `failed`。
225
+ 每个验收检查项包含 `id`、可选或追踪模式下必需的 `requirementId`、`specSnippet`、`scene`、`reportModule`、`reportGroup`、`reportTitle`、`reportMethod`、`errorType`(`功能缺陷` 或 `UI视觉缺陷`)、`expect`、`actual`、可选的 `evidencePath` 和 `isPass`。Checker 合并 Runner 检查时必须原样保留需求编号和四个报告字段;同一 `reportGroup` 的 `reportModule`、`reportTitle` 和 `reportMethod` 必须一致。每条原子 spec 检查只出现一次。证据不完整时验收者状态为 `awaiting-decision`;证据完整但存在失败检查时为 `failed`。
213
226
 
214
227
  `checker-result.md` 固定使用中文标题 `E2E UI 验收结果`、`证据摘要`、`视觉结果`、`运行结果` 和 `证据路径`;表头和展示状态也使用中文。机器状态可在中文结论旁以反引号保留。
215
228
 
216
229
  ## Excel 报告分组
217
230
 
231
+ 追踪模式下,底层检查和语义分组保留 `requirementIds` 供审计;可见 Excel 仍按产品功能展示,不增加 REQ ID 列。
232
+
218
233
  Excel 只包含一个名为 `自测报告` 的工作表,每行代表一个真实 UI 组件或完整业务功能。不得显示 story ID、scene 名、历史场景或原子明细工作表。桌面端和移动端代表截图位于同一功能行。新证据直接使用报告字段聚合;历史证据缺少这些字段时,导出 Skill 必须阅读 `spec.md` 和验收证据,生成 `report/report-groups.json` 语义分组清单。清单必须绑定当前 spec SHA-256、Runner iteration 和冻结场景 SHA-256,并完整且不重复地覆盖全部 story;导出器不得以 scene、story 顺序或截图名称猜测业务分组。
219
234
 
220
235
  代码审计结果:
@@ -14,10 +14,14 @@ description: 在首版实现可运行后,为 Autobest Delivery 运行器编写
14
14
  - 可运行的首版实现。
15
15
  - 仅在修订回合读取 `test_defect` 人工决议及其指向的 Blocked runner/checker 证据。
16
16
 
17
+ 存在 `featureDir/spec-traceability.json` 时必须读取并校验;它定义本场景允许使用的完整 active REQ ID 集合。
18
+
17
19
  产品预期只由 spec 决定。按契约解析具体执行事实。存在通配路由,或者 spec 未直接写出命令、视口或定位器,并不自动构成阻断。
18
20
 
19
21
  ## 流程
20
22
 
23
+ 追踪模式下,每项运行检查和视觉映射都写入所属 `requirementId`,不得创建或重编号。`metadata` 必须声明 `traceabilitySchemaVersion: 1` 和完整、唯一的 `requirementIds`;冻结前确认每个 active ID 至少有一项运行或视觉覆盖。
24
+
21
25
  1. 将每条原子 spec 预期准确映射到一个运行检查或一个视觉映射。为每项分配通用的 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,让同一 UI 组件、页面位置或完整用户操作流的文案、样式、布局、响应式行为和相关功能检查在报告中合并;同组的模块、标题和检查方式必须完全一致。不同组件、独立业务能力、状态转换或风险边界保持不同组。分组只表达当前 spec 的功能语义,不得按 scene 或执行顺序分组,不得写死特定页面族词汇或依赖导出器猜测业务语义。覆盖所有必需页面变体、状态转换、空态或错误态、导航结果和响应式变体。
22
26
  2. 解析并探测具体 URL、启动行为、受控 API 数据、持久化状态和面向用户的稳定定位器。通过解码后的状态或受控 fixture 确认通配路由变体。
23
27
  3. 首次编写只写入 `featureDir/e2e/e2e.feature.mjs`,保持旧 `.spec.ts` 不变。该文件不得包含 import,只导出 `metadata` 和一个默认异步函数,并且只能使用运行器注入的 `scene`、`page`、`expect`、`check`、`route`、`capture`、`artifact`、`baseUrl` 和 `parseUrl`。新建或修订场景在 metadata 中声明 `reportSchemaVersion: 1`,使运行器校验所有检查和视觉映射的通用报告分组字段。每个 `metadata.scenes` 项通过 `scene(id, callback)` 恰好执行一次;每个 scene 自包含页面导航、API fixture 和必要状态。冻结后只有用户通过 `resolve_blocked_run` 明确选择 `test_defect` 才能修订;修订结果必须记录旧、新 SHA-256 和决议证据路径。
@@ -16,6 +16,8 @@ description: 使用 Autobest Delivery MCP 运行器在可见浏览器中独立
16
16
 
17
17
  ## 流程
18
18
 
19
+ 存在 `spec-traceability.json` 时,先确认冻结场景声明的 REQ ID 与 active 集合完全一致。合并 Runner 和视觉结果时原样保留每项 `requirementId`,逐个汇总 passed、failed 或 blocked;未知编号、缺失编号或 active 需求无证据都属于证据链阻断,不能判为通过。
20
+
19
21
  1. 建立原子 spec 检查清单,确认冻结运行检查和 metadata 视觉映射完整且不重复地划分了全部检查项。`reportSchemaVersion: 1` 的场景须核对每个运行检查包含 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,同组字段一致且表达真实组件或完整功能,而不是 E2E scene;同时核对视觉映射的同组字段和 `reportDevice` 正确区分桌面端与移动端。历史场景没有报告 schema 时不据此阻断。
20
22
  2. 调用 `autobest-delivery` MCP 的 `check_environment` 工具并传入 `headed: true`。环境未就绪时,以返回的版本和错误证据形成 `tool` 阻断;不得降级使用通用 Playwright MCP 或目标仓库依赖。
21
23
  3. 只解析并启动仓库已声明且本次必需的开发服务,记录其 PID,并确认基础 URL。复用冻结的 `1400px` 和 `375px` 视口及受控路由或 API 状态。
@@ -14,6 +14,8 @@ description: 从已完成或已阻断的 Autobest Delivery Maker、Runner 和 Ch
14
14
 
15
15
  ## 执行
16
16
 
17
+ 追踪模式下,底层检查、视觉映射和 `report/report-groups.json` 必须保留真实 `requirementIds` 供审计;Excel 可见列仍按产品功能展示,不新增编号列。
18
+
17
19
  1. 将功能目录和可选输出路径解析为绝对路径,并使用 `git rev-parse --show-toplevel` 取得工作区。
18
20
  2. 从当前 Skill 路径定位插件根目录,不依赖业务仓库中的 `plugins/` 路径。
19
21
  3. 执行:
@@ -51,6 +53,7 @@ node <plugin-root>/scripts/export-delivery-report.mjs <feature-dir> \
51
53
  "module": "页面或模块",
52
54
  "title": "组件或完整功能名称",
53
55
  "method": "合并描述该功能的文案、视觉、布局、响应式和行为检查方式。",
56
+ "requirementIds": ["REQ-PD-001"],
54
57
  "storyIds": ["story-01", "story-02"],
55
58
  "desktopCapture": "可选的桌面截图文件名或证据路径",
56
59
  "mobileCapture": "可选的移动端截图文件名或证据路径"