@openprd/cli 0.1.11 → 0.1.18

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 (134) hide show
  1. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  2. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  3. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  4. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  5. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  6. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  7. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  8. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  13. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  14. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  19. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  20. package/.openprd/changes/openprd/design.md +46 -0
  21. package/.openprd/changes/openprd/proposal.md +36 -0
  22. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  23. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  24. package/.openprd/changes/openprd/tasks.md +242 -0
  25. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  26. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  27. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  28. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  29. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  30. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  31. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  32. package/.openprd/changes/test-strategy-router/design.md +65 -0
  33. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  34. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  35. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  36. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  37. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  38. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  39. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  40. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  41. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  42. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  43. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  44. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  45. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  46. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  47. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  48. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  49. package/.openprd/design/README.md +11 -6
  50. package/.openprd/design/README_EN.md +4 -1
  51. package/.openprd/design/active/asset-spec.md +6 -0
  52. package/.openprd/design/active/direction-plan.md +5 -5
  53. package/.openprd/design/active/selected-direction.md +2 -0
  54. package/.openprd/design/anti-slop.md +26 -1
  55. package/.openprd/design/assets/surface-presets.md +6 -0
  56. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  57. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  58. package/.openprd/design/recipes/content-experience.md +8 -1
  59. package/.openprd/design/recipes/operational-tool.md +7 -0
  60. package/.openprd/design/recipes/product-launch.md +8 -1
  61. package/.openprd/design/templates/README.md +6 -3
  62. package/.openprd/design/templates/README_EN.md +6 -3
  63. package/.openprd/design/templates/ops-dashboard.html +8 -1
  64. package/.openprd/design/templates/product-launch.html +2 -1
  65. package/.openprd/design/themes/theme-catalog.json +23 -5
  66. package/.openprd/engagements/active/prd.md +103 -100
  67. package/.openprd/standards/config.json +7 -1
  68. package/AGENTS.md +13 -3
  69. package/README.md +107 -14
  70. package/README_EN.md +131 -13
  71. package/package.json +1 -1
  72. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  73. package/skills/openprd-diagram-review/SKILL.md +27 -2
  74. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  75. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  76. package/skills/openprd-frontend-design/SKILL.md +44 -20
  77. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  78. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  79. package/skills/openprd-harness/SKILL.md +22 -16
  80. package/skills/openprd-quality/SKILL.md +10 -8
  81. package/skills/openprd-requirement-intake/SKILL.md +2 -0
  82. package/skills/openprd-router/SKILL.md +2 -1
  83. package/skills/openprd-shared/SKILL.md +26 -16
  84. package/skills/openprd-standards/SKILL.md +4 -2
  85. package/src/agent-canonical-content.js +808 -0
  86. package/src/agent-integration.js +258 -777
  87. package/src/canvas-app.html.js +2910 -0
  88. package/src/canvas-i18n.js +274 -0
  89. package/src/canvas-workspace.js +2271 -0
  90. package/src/cli/args.js +72 -4
  91. package/src/cli/basic-print.js +3 -0
  92. package/src/cli/canvas-print.js +106 -0
  93. package/src/cli/doctor-print.js +20 -0
  94. package/src/cli/print.js +2 -0
  95. package/src/cli/quality-commands.js +222 -0
  96. package/src/cli/quality-print.js +28 -2
  97. package/src/cli/run-print.js +5 -0
  98. package/src/cli/workflow-print.js +3 -0
  99. package/src/codex-app-server-relay.js +251 -0
  100. package/src/codex-hook-runner-template.mjs +957 -59
  101. package/src/design-starter-support.js +2 -2
  102. package/src/design-starter.js +39 -3
  103. package/src/dev-standards.js +125 -10
  104. package/src/diagram-core.js +6 -20
  105. package/src/discovery.js +2 -8
  106. package/src/docs-compact.js +125 -0
  107. package/src/execution-strategy.js +1 -1
  108. package/src/fleet.js +9 -3
  109. package/src/guide.js +227 -0
  110. package/src/html-artifacts.js +428 -7
  111. package/src/knowledge.js +45 -11
  112. package/src/language-policy.js +63 -3
  113. package/src/learning-review.js +1 -1
  114. package/src/loop.js +1 -1
  115. package/src/openprd.js +68 -131
  116. package/src/openspec/change-validate.js +2 -2
  117. package/src/openspec/constants.js +6 -7
  118. package/src/openspec/execute.js +2 -2
  119. package/src/openspec/generate.js +318 -102
  120. package/src/openspec/migration.js +168 -0
  121. package/src/openspec/paths.js +1 -22
  122. package/src/prd-core.js +1 -1
  123. package/src/quality-visual-review.js +257 -14
  124. package/src/quality.js +46 -8
  125. package/src/review-presentation.js +60 -0
  126. package/src/run-harness.js +41 -3
  127. package/src/session-evidence.js +363 -0
  128. package/src/standards.js +64 -0
  129. package/src/test-strategy.js +7 -4
  130. package/src/visual-compare-centering.js +622 -0
  131. package/src/visual-compare-core.js +1123 -0
  132. package/src/visual-compare.js +620 -698
  133. package/src/workspace-core.js +94 -4
  134. package/src/workspace-workflow.js +46 -1
@@ -0,0 +1,168 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT,
5
+ LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT,
6
+ LEGACY_OPENPRD_CHANGE_ROOT,
7
+ OPENPRD_ACCEPTED_SPEC_ROOT,
8
+ OPENPRD_ARCHIVE_CHANGE_ROOT,
9
+ OPENPRD_CHANGE_ROOT,
10
+ } from './constants.js';
11
+
12
+ function cjoin(...parts) {
13
+ return path.join(...parts);
14
+ }
15
+
16
+ async function exists(filePath) {
17
+ return fs.access(filePath).then(() => true).catch(() => false);
18
+ }
19
+
20
+ function relativeProjectPath(projectRoot, filePath) {
21
+ return path.relative(projectRoot, filePath) || '.';
22
+ }
23
+
24
+ async function removeDirectoryIfEmpty(dirPath) {
25
+ const entries = await fs.readdir(dirPath).catch(() => null);
26
+ if (entries && entries.length === 0) {
27
+ await fs.rmdir(dirPath);
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ async function sameFileContent(leftPath, rightPath) {
34
+ const [left, right] = await Promise.all([
35
+ fs.readFile(leftPath),
36
+ fs.readFile(rightPath),
37
+ ]);
38
+ return left.equals(right);
39
+ }
40
+
41
+ async function movePath(sourcePath, targetPath) {
42
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
43
+ try {
44
+ await fs.rename(sourcePath, targetPath);
45
+ } catch (error) {
46
+ if (error?.code !== 'EXDEV') {
47
+ throw error;
48
+ }
49
+ const stat = await fs.stat(sourcePath);
50
+ await fs.cp(sourcePath, targetPath, { recursive: stat.isDirectory(), force: false, errorOnExist: true });
51
+ await fs.rm(sourcePath, { recursive: stat.isDirectory(), force: true });
52
+ }
53
+ }
54
+
55
+ async function mergeLegacyTree(projectRoot, sourcePath, targetPath, changes) {
56
+ const sourceStat = await fs.stat(sourcePath).catch(() => null);
57
+ if (!sourceStat) {
58
+ return { migrated: 0, conflicts: 0 };
59
+ }
60
+
61
+ const targetStat = await fs.stat(targetPath).catch(() => null);
62
+ if (!targetStat) {
63
+ await movePath(sourcePath, targetPath);
64
+ changes.push({
65
+ path: relativeProjectPath(projectRoot, targetPath),
66
+ from: relativeProjectPath(projectRoot, sourcePath),
67
+ status: 'migrated',
68
+ });
69
+ return { migrated: 1, conflicts: 0 };
70
+ }
71
+
72
+ if (sourceStat.isDirectory() && targetStat.isDirectory()) {
73
+ let migrated = 0;
74
+ let conflicts = 0;
75
+ const entries = await fs.readdir(sourcePath, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ const sourceChild = cjoin(sourcePath, entry.name);
78
+ const targetChild = cjoin(targetPath, entry.name);
79
+ const result = await mergeLegacyTree(projectRoot, sourceChild, targetChild, changes);
80
+ migrated += result.migrated;
81
+ conflicts += result.conflicts;
82
+ }
83
+ if (await removeDirectoryIfEmpty(sourcePath)) {
84
+ changes.push({
85
+ path: relativeProjectPath(projectRoot, sourcePath),
86
+ status: 'removed-empty-legacy-dir',
87
+ });
88
+ }
89
+ return { migrated, conflicts };
90
+ }
91
+
92
+ if (sourceStat.isFile() && targetStat.isFile() && await sameFileContent(sourcePath, targetPath)) {
93
+ await fs.rm(sourcePath, { force: true });
94
+ changes.push({
95
+ path: relativeProjectPath(projectRoot, sourcePath),
96
+ target: relativeProjectPath(projectRoot, targetPath),
97
+ status: 'removed-duplicate-legacy-file',
98
+ });
99
+ return { migrated: 0, conflicts: 0 };
100
+ }
101
+
102
+ changes.push({
103
+ path: relativeProjectPath(projectRoot, sourcePath),
104
+ target: relativeProjectPath(projectRoot, targetPath),
105
+ status: 'conflict-kept-legacy',
106
+ });
107
+ return { migrated: 0, conflicts: 1 };
108
+ }
109
+
110
+ export async function hasLegacyOpenPrdArtifacts(projectRoot) {
111
+ const roots = [
112
+ cjoin(projectRoot, ...LEGACY_OPENPRD_CHANGE_ROOT),
113
+ cjoin(projectRoot, ...LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT),
114
+ cjoin(projectRoot, ...LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT),
115
+ ];
116
+ for (const root of roots) {
117
+ if (await exists(root)) {
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ export async function migrateLegacyOpenPrdArtifacts(projectRoot) {
125
+ const legacyRoot = cjoin(projectRoot, 'openprd');
126
+ const changes = [];
127
+ let migrated = 0;
128
+ let conflicts = 0;
129
+
130
+ const pairs = [
131
+ [LEGACY_OPENPRD_CHANGE_ROOT, OPENPRD_CHANGE_ROOT],
132
+ [LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT, OPENPRD_ACCEPTED_SPEC_ROOT],
133
+ [LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT, OPENPRD_ARCHIVE_CHANGE_ROOT],
134
+ ];
135
+
136
+ for (const [legacyParts, targetParts] of pairs) {
137
+ const result = await mergeLegacyTree(
138
+ projectRoot,
139
+ cjoin(projectRoot, ...legacyParts),
140
+ cjoin(projectRoot, ...targetParts),
141
+ changes,
142
+ );
143
+ migrated += result.migrated;
144
+ conflicts += result.conflicts;
145
+ }
146
+
147
+ if (await exists(legacyRoot)) {
148
+ await removeDirectoryIfEmpty(cjoin(legacyRoot, 'archive')).catch(() => false);
149
+ if (await removeDirectoryIfEmpty(legacyRoot).catch(() => false)) {
150
+ changes.push({ path: 'openprd', status: 'removed-empty-legacy-root' });
151
+ } else if (changes.length > 0) {
152
+ changes.push({ path: 'openprd', status: 'legacy-root-kept-nonempty' });
153
+ }
154
+ }
155
+
156
+ return {
157
+ ok: conflicts === 0,
158
+ action: 'migrate-legacy-openprd-artifacts',
159
+ projectRoot,
160
+ migrated,
161
+ conflicts,
162
+ changed: changes.length > 0,
163
+ changes,
164
+ errors: conflicts > 0
165
+ ? ['旧 openprd 目录中存在与 .openprd 目标路径同名但内容不同的文件,已保留旧文件等待人工处理。']
166
+ : [],
167
+ };
168
+ }
@@ -1,9 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
- LEGACY_OPENSPEC_CHANGE_ROOT,
5
- LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH,
6
- LEGACY_OPENSPEC_DISCOVERY_DIR,
7
4
  OPENPRD_ACCEPTED_SPEC_ROOT,
8
5
  OPENPRD_ARCHIVE_CHANGE_ROOT,
9
6
  OPENPRD_CHANGE_ROOT,
@@ -23,34 +20,18 @@ export function openPrdDiscoveryDir(projectRoot) {
23
20
  return cjoin(projectRoot, ...OPENPRD_DISCOVERY_DIR);
24
21
  }
25
22
 
26
- export function legacyOpenSpecDiscoveryDir(projectRoot) {
27
- return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_DIR);
28
- }
29
-
30
23
  export function openPrdDiscoveryConfigPath(projectRoot) {
31
24
  return cjoin(projectRoot, ...OPENPRD_DISCOVERY_CONFIG_PATH);
32
25
  }
33
26
 
34
- export function legacyOpenSpecDiscoveryConfigPath(projectRoot) {
35
- return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH);
36
- }
37
-
38
27
  export async function readDiscoveryConfig(projectRoot, readJson) {
39
- const primary = openPrdDiscoveryConfigPath(projectRoot);
40
- const legacy = legacyOpenSpecDiscoveryConfigPath(projectRoot);
41
- return (await readJson(primary).catch(() => null))
42
- ?? (await readJson(legacy).catch(() => null))
43
- ?? {};
28
+ return (await readJson(openPrdDiscoveryConfigPath(projectRoot)).catch(() => null)) ?? {};
44
29
  }
45
30
 
46
31
  export function openPrdChangeRoot(projectRoot) {
47
32
  return cjoin(projectRoot, ...OPENPRD_CHANGE_ROOT);
48
33
  }
49
34
 
50
- export function legacyOpenSpecChangeRoot(projectRoot) {
51
- return cjoin(projectRoot, ...LEGACY_OPENSPEC_CHANGE_ROOT);
52
- }
53
-
54
35
  export function openPrdAcceptedSpecRoot(projectRoot) {
55
36
  return cjoin(projectRoot, ...OPENPRD_ACCEPTED_SPEC_ROOT);
56
37
  }
@@ -62,7 +43,6 @@ export function openPrdArchiveChangeRoot(projectRoot) {
62
43
  export async function resolveChangeDir(projectRoot, changeId) {
63
44
  const candidates = [
64
45
  cjoin(openPrdChangeRoot(projectRoot), changeId),
65
- cjoin(legacyOpenSpecChangeRoot(projectRoot), changeId),
66
46
  cjoin(openPrdArchiveChangeRoot(projectRoot), changeId),
67
47
  ];
68
48
  for (const candidate of candidates) {
@@ -76,7 +56,6 @@ export async function resolveChangeDir(projectRoot, changeId) {
76
56
  export async function listChangeDirs(projectRoot) {
77
57
  const roots = [
78
58
  { root: openPrdChangeRoot(projectRoot), source: 'openprd', archived: false },
79
- { root: legacyOpenSpecChangeRoot(projectRoot), source: 'legacy-openspec', archived: false },
80
59
  { root: openPrdArchiveChangeRoot(projectRoot), source: 'openprd-archive', archived: true },
81
60
  ];
82
61
  const changes = [];
package/src/prd-core.js CHANGED
@@ -298,7 +298,7 @@ export function buildPrdSnapshot(ws, options = {}) {
298
298
  handoff: {
299
299
  owner: pickValue(options.handoffOwner, state.handoffOwner, owner),
300
300
  nextStep: pickValue(options.nextStep, state.nextStep, '评审已生成的 PRD,并准备交接。'),
301
- targetSystem: pickValue(options.targetSystem, state.targetSystem, 'OpenSpec'),
301
+ targetSystem: pickValue(options.targetSystem, state.targetSystem, 'OpenPrd'),
302
302
  },
303
303
  typeSpecific: buildTypeSpecificSection(productType, state, options),
304
304
  };
@@ -18,6 +18,15 @@ function inferVisualReviewMode(value) {
18
18
  if (normalized.includes('visual-parallel-board') || normalized.includes('parallel-board') || normalized.includes('experiment-board')) {
19
19
  return 'parallel-board';
20
20
  }
21
+ if (normalized.includes('visual-verification-board') || normalized.includes('verification-board') || normalized.includes('screenshot-evidence-board') || normalized.includes('screenshot-board')) {
22
+ return 'verification-board';
23
+ }
24
+ if (normalized.includes('visual-alignment-board') || normalized.includes('alignment-board') || normalized.includes('grid-alignment')) {
25
+ return 'alignment-board';
26
+ }
27
+ if (normalized.includes('visual-centering-board') || normalized.includes('centering-board') || normalized.includes('center-board')) {
28
+ return 'centering-board';
29
+ }
21
30
  if (normalized.includes('visual-before-after') || normalized.includes('before-after')) {
22
31
  return 'before-after';
23
32
  }
@@ -73,6 +82,9 @@ export async function listVisualReviewArtifacts(projectRoot) {
73
82
  metadataPath: entry.relativePath,
74
83
  mode: inferredMode,
75
84
  labels: payload.labels ?? existing.labels ?? null,
85
+ alignmentScope: payload.alignmentScope ?? existing.alignmentScope ?? null,
86
+ centering: payload.centering ?? existing.centering ?? null,
87
+ groups: Array.isArray(payload.groups) ? payload.groups : (existing.groups ?? null),
76
88
  generatedAt: payload.generatedAt ?? existing.generatedAt ?? null,
77
89
  mtimeMs: stat?.mtimeMs ?? existing.mtimeMs ?? 0,
78
90
  });
@@ -138,29 +150,186 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
138
150
  'parallel board',
139
151
  'parallel experiment',
140
152
  'experiment board',
141
- 'evidence board',
142
153
  '并行实验',
143
154
  '并行方向',
144
155
  '多方向实验',
145
156
  '方案对比板',
146
- '证据板',
157
+ ];
158
+ const verificationTokens = [
159
+ 'verification board',
160
+ 'verification-board',
161
+ 'screenshot board',
162
+ 'screenshot-evidence-board',
163
+ 'visual-verification-board',
164
+ 'computer screenshot',
165
+ 'computer use screenshot',
166
+ 'browser screenshot',
167
+ 'playwright screenshot',
168
+ 'screencapture',
169
+ '普通截图',
170
+ '实测截图',
171
+ '截图实测',
172
+ '截图证据',
173
+ '截图验收',
174
+ 'Computer 实测',
175
+ 'Browser 实测',
176
+ '运行态截图',
177
+ ];
178
+ const alignmentTokens = [
179
+ 'alignment board',
180
+ 'alignment-board',
181
+ 'grid alignment',
182
+ 'grid-alignment',
183
+ 'baseline',
184
+ 'baseline spread',
185
+ 'same slot',
186
+ 'repeated card',
187
+ 'homogeneous card',
188
+ 'list card',
189
+ 'card grid',
190
+ 'table layout',
191
+ '对齐辅助线',
192
+ '辅助线',
193
+ '网格对齐',
194
+ '基线',
195
+ '坐标偏差',
196
+ '同构',
197
+ '重复单元',
198
+ '相同槽位',
199
+ '相同文案类型',
200
+ '列表卡片',
201
+ '卡片列表',
202
+ '卡片网格',
203
+ '网格卡片',
204
+ '网格列表',
205
+ '列表网格',
206
+ '表格',
207
+ '排版对齐',
208
+ '排版漂移',
209
+ '布局漂移',
210
+ '对齐有问题',
211
+ '对齐问题',
212
+ '对齐不准',
213
+ '对不齐',
214
+ '不对齐',
215
+ '没有对齐',
216
+ '没对齐',
217
+ '左右偏差',
218
+ '上下偏差',
219
+ '横向偏差',
220
+ '竖向偏差',
221
+ ];
222
+ const centeringTokens = [
223
+ 'centering board',
224
+ 'centering-board',
225
+ 'center board',
226
+ 'center-board',
227
+ 'visual-centering-board',
228
+ 'canvas center',
229
+ 'visual centroid',
230
+ 'bbox center',
231
+ 'bounding box center',
232
+ 'active bounds',
233
+ 'single element',
234
+ 'inside centered',
235
+ 'internal centering',
236
+ 'logo center',
237
+ 'logo centered',
238
+ 'logo internal',
239
+ 'icon center',
240
+ 'icon centered',
241
+ 'icon internal',
242
+ 'avatar center',
243
+ 'badge center',
244
+ 'button graphic',
245
+ '内部居中',
246
+ '内部不居中',
247
+ '居中判定',
248
+ '居中识别',
249
+ '居中评估',
250
+ '视觉重心',
251
+ '主体外接框',
252
+ '外接框中心',
253
+ '画布中心',
254
+ '单元素',
255
+ '单个元素',
256
+ '单一元素',
257
+ '图标内部',
258
+ '图片内部',
259
+ '素材内部',
260
+ '按钮图形',
261
+ '偏心',
262
+ '不居中',
263
+ '没居中',
264
+ '左偏',
265
+ '右偏',
266
+ '上飘',
267
+ '下沉',
268
+ ];
269
+ const contentSlotAlignmentTokens = [
270
+ 'content slot',
271
+ 'internal slot',
272
+ 'same slot',
273
+ 'title slot',
274
+ 'price slot',
275
+ 'button slot',
276
+ 'card content',
277
+ 'repeated card',
278
+ 'homogeneous card',
279
+ 'list card',
280
+ 'card grid',
281
+ '标题',
282
+ '副标题',
283
+ '描述',
284
+ '正文',
285
+ '标签',
286
+ '状态',
287
+ '价格',
288
+ '按钮',
289
+ '操作区',
290
+ '图标',
291
+ '内容槽位',
292
+ '内部槽位',
293
+ '相同槽位',
294
+ '相同文案类型',
295
+ '相同组件槽位',
296
+ '列表卡片',
297
+ '卡片列表',
298
+ '卡片网格',
299
+ '网格卡片',
300
+ '网格列表',
301
+ '列表网格',
147
302
  ];
148
303
  const expectsReferenceCompare = includesAny(haystack, referenceTokens);
149
304
  const expectsFocusBoard = includesAny(haystack, focusTokens);
150
305
  const expectsParallelBoard = includesAny(haystack, parallelTokens);
306
+ const expectsVerificationBoard = includesAny(haystack, verificationTokens);
307
+ const expectsAlignmentBoard = includesAny(haystack, alignmentTokens);
308
+ const expectsCenteringBoard = includesAny(haystack, centeringTokens);
309
+ const expectsContentSlotAlignment = expectsAlignmentBoard && includesAny(haystack, contentSlotAlignmentTokens);
151
310
  const referenceArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'reference-actual');
152
311
  const beforeAfterArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'before-after');
153
312
  const focusArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'focus-board');
154
313
  const parallelArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'parallel-board');
314
+ const verificationArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'verification-board');
315
+ const alignmentArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'alignment-board');
316
+ const centeringArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'centering-board');
317
+ const contentSlotAlignmentArtifacts = alignmentArtifacts.filter(hasContentSlotAlignmentEvidence);
155
318
  let matchingArtifacts;
156
- if (expectsParallelBoard) {
319
+ if (expectsCenteringBoard) {
320
+ matchingArtifacts = centeringArtifacts;
321
+ } else if (expectsAlignmentBoard) {
322
+ matchingArtifacts = alignmentArtifacts;
323
+ } else if (expectsVerificationBoard) {
324
+ matchingArtifacts = verificationArtifacts;
325
+ } else if (expectsParallelBoard) {
157
326
  matchingArtifacts = parallelArtifacts;
158
327
  } else if (expectsFocusBoard) {
159
328
  matchingArtifacts = focusArtifacts;
160
329
  } else if (expectsReferenceCompare) {
161
330
  matchingArtifacts = referenceArtifacts;
162
331
  } else {
163
- matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts];
332
+ matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts, ...verificationArtifacts, ...alignmentArtifacts, ...centeringArtifacts];
164
333
  }
165
334
  const evidenceSources = matchingArtifacts.slice(0, 12).map((artifact) => ({
166
335
  path: artifact.path,
@@ -170,15 +339,31 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
170
339
  ? 'visual-review/before-after'
171
340
  : artifact.mode === 'focus-board'
172
341
  ? 'visual-review/focus-board'
173
- : 'visual-review/parallel-board',
342
+ : artifact.mode === 'parallel-board'
343
+ ? 'visual-review/parallel-board'
344
+ : artifact.mode === 'verification-board'
345
+ ? 'visual-review/verification-board'
346
+ : artifact.mode === 'alignment-board'
347
+ ? 'visual-review/alignment-board'
348
+ : 'visual-review/centering-board',
174
349
  }));
175
350
  const warnings = [];
351
+ const missingContentSlotAlignment = relevant
352
+ && expectsContentSlotAlignment
353
+ && alignmentArtifacts.length > 0
354
+ && contentSlotAlignmentArtifacts.length === 0;
176
355
 
177
356
  if (relevant && matchingArtifacts.length === 0) {
178
357
  warnings.push(
179
- expectsParallelBoard
358
+ expectsCenteringBoard
359
+ ? '检测到 logo、图标、头像、徽标、图片或按钮图形等单个元素的内部居中/视觉重心语义,但未看到本次内部居中证据板。单张截图或主观“看起来居中”不能替代画布中心、主体外接框和视觉重心偏移量测。'
360
+ : expectsAlignmentBoard
361
+ ? '检测到同构列表、卡片、网格、表格或对齐反馈语义,但未看到本次对齐辅助线证据板。新功能开发中只要有重复同构单元,也应同时量测容器轨道和内部内容槽位的坐标偏差。'
362
+ : expectsParallelBoard
180
363
  ? '检测到界面视觉改动且用户在比较多方向实验,但未看到本次并行实验证据板。'
181
- : expectsFocusBoard
364
+ : expectsVerificationBoard
365
+ ? '检测到普通截图、Computer/Browser 实测或运行态截图语义,但未看到本次截图实测证据板。普通截图只能作为原始素材,不能单独替代视觉收口拼图。'
366
+ : expectsFocusBoard
182
367
  ? '检测到界面视觉改动且用户在关注局部细节,但未看到本次局部焦点证据板。'
183
368
  : expectsReferenceCompare
184
369
  ? '检测到界面视觉改动且已有参考图/设计稿语义,但未看到本次“效果图 / 实现截图”对比证据。'
@@ -190,24 +375,46 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
190
375
  warnings.push('当前有视觉证据,但局部细节仍建议补一份局部焦点证据板,方便围绕编号区域复核。');
191
376
  } else if (relevant && expectsParallelBoard && parallelArtifacts.length === 0 && matchingArtifacts.length > 0) {
192
377
  warnings.push('当前有视觉证据,但多方向实验仍建议补一份并行实验证据板,把方案和指标放到同一板里审查。');
378
+ } else if (relevant && expectsVerificationBoard && verificationArtifacts.length === 0 && matchingArtifacts.length > 0) {
379
+ warnings.push('当前有视觉证据,但普通截图或 Computer 实测仍建议补一份截图实测证据板,把截图、实测路径和检查点拼到同一张图里。');
380
+ } else if (relevant && expectsAlignmentBoard && alignmentArtifacts.length === 0 && matchingArtifacts.length > 0) {
381
+ warnings.push('当前有视觉证据,但同构列表、卡片、网格或表格仍建议补一份对齐辅助线证据板,把相同槽位的 spread 放到同一张图里。');
382
+ } else if (relevant && expectsCenteringBoard && centeringArtifacts.length === 0 && matchingArtifacts.length > 0) {
383
+ warnings.push('当前有视觉证据,但单元素内部居中仍建议补一份内部居中证据板,把画布中心、主体外接框和视觉重心偏移放到同一张图里。');
384
+ } else if (missingContentSlotAlignment) {
385
+ warnings.push('当前只看到对齐辅助线证据板,但没有看到标题、副标题、描述、标签、状态、价格、按钮或图标等内部内容槽位量测。卡片/列表/网格不能只量外框、列宽或行顶。');
193
386
  }
194
387
 
195
388
  const summary = !relevant
196
389
  ? '当前场景未要求视觉评审证据'
197
390
  : matchingArtifacts.length > 0
198
- ? (
199
- expectsParallelBoard
391
+ ? (
392
+ missingContentSlotAlignment
393
+ ? '已找到对齐辅助线证据板,但缺少内部内容槽位量测'
394
+ : expectsCenteringBoard
395
+ ? `已找到 ${matchingArtifacts.length} 份内部居中证据板`
396
+ : expectsAlignmentBoard
397
+ ? `已找到 ${matchingArtifacts.length} 份对齐辅助线证据板`
398
+ : expectsParallelBoard
200
399
  ? `已找到 ${matchingArtifacts.length} 份并行实验证据板`
201
- : expectsFocusBoard
400
+ : expectsVerificationBoard
401
+ ? `已找到 ${matchingArtifacts.length} 份截图实测证据板`
402
+ : expectsFocusBoard
202
403
  ? `已找到 ${matchingArtifacts.length} 份局部焦点证据板`
203
404
  : expectsReferenceCompare
204
405
  ? `已找到 ${matchingArtifacts.length} 份效果图 / 实现截图对比证据`
205
- : `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点或修改前后自检证据`
406
+ : `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点、截图实测或修改前后自检证据`
206
407
  )
207
408
  : (
208
- expectsParallelBoard
409
+ expectsCenteringBoard
410
+ ? '未找到本次内部居中证据板'
411
+ : expectsAlignmentBoard
412
+ ? '未找到本次对齐辅助线证据板'
413
+ : expectsParallelBoard
209
414
  ? '未找到本次并行实验证据板'
210
- : expectsFocusBoard
415
+ : expectsVerificationBoard
416
+ ? '未找到本次截图实测证据板'
417
+ : expectsFocusBoard
211
418
  ? '未找到本次局部焦点证据板'
212
419
  : expectsReferenceCompare
213
420
  ? '未找到本次效果图 / 实现截图对比证据'
@@ -215,13 +422,19 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
215
422
  );
216
423
 
217
424
  return {
218
- status: !relevant || matchingArtifacts.length > 0 ? 'pass' : 'needs-evidence',
425
+ status: !relevant || (matchingArtifacts.length > 0 && !missingContentSlotAlignment) ? 'pass' : 'needs-evidence',
219
426
  relevant,
220
427
  expectsReferenceCompare,
221
428
  expectsFocusBoard,
222
429
  expectsParallelBoard,
430
+ expectsVerificationBoard,
431
+ expectsAlignmentBoard,
432
+ expectsCenteringBoard,
433
+ expectsContentSlotAlignment,
223
434
  artifacts: visualArtifacts,
224
435
  matchingArtifacts,
436
+ contentSlotAlignmentArtifacts,
437
+ centeringArtifacts,
225
438
  warnings,
226
439
  evidence: {
227
440
  present: matchingArtifacts.length > 0,
@@ -231,6 +444,36 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
231
444
  };
232
445
  }
233
446
 
447
+ function hasContentSlotAlignmentEvidence(artifact) {
448
+ const haystack = JSON.stringify([
449
+ artifact.path,
450
+ artifact.metadataPath,
451
+ artifact.labels,
452
+ artifact.alignmentScope,
453
+ artifact.groups,
454
+ ]).toLowerCase();
455
+ return [
456
+ 'content-slot',
457
+ 'content slot',
458
+ 'internal slot',
459
+ '内容槽位',
460
+ '内部槽位',
461
+ '相同槽位',
462
+ '相同文案类型',
463
+ '相同组件槽位',
464
+ '标题',
465
+ '副标题',
466
+ '描述',
467
+ '正文',
468
+ '标签',
469
+ '状态',
470
+ '价格',
471
+ '按钮',
472
+ '操作区',
473
+ '图标',
474
+ ].some((token) => haystack.includes(String(token).toLowerCase()));
475
+ }
476
+
234
477
  export {
235
478
  VISUAL_REVIEW_DIR,
236
479
  VISUAL_REVIEW_SCHEMA,