@haaaiawd/anws 2.0.5 → 2.1.1

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.
package/bin/cli.js CHANGED
@@ -22,8 +22,7 @@ COMMANDS
22
22
  OPTIONS
23
23
  -v, --version Print version number
24
24
  -h, --help Show this help message
25
- --target Target AI IDE(s) for \`init\`, comma-separated (${TARGET_IDS.join(', ')})
26
- --check Preview grouped update diffs without writing files or rebuilding install-lock state
25
+ --target Target AI IDE(s) for init, comma-separated (${TARGET_IDS.join(', ')})
27
26
 
28
27
  SUPPORTED TARGETS
29
28
  windsurf workflows + skills
@@ -40,8 +39,7 @@ SUPPORTED TARGETS
40
39
  EXAMPLES
41
40
  anws init # Choose target IDEs and install their managed workflow projections
42
41
  anws init --target windsurf,codex,opencode
43
- anws update # Update all matched targets from install-lock, fallback scan, or drift repair
44
- anws update --check # Preview grouped changes per target without writing files
42
+ anws update # One-click update for all matched targets from install-lock, fallback scan, or drift repair
45
43
  `.trimStart();
46
44
 
47
45
  // ─── 参数解析 ─────────────────────────────────────────────────────────────────
@@ -90,7 +88,15 @@ async function main() {
90
88
  break;
91
89
 
92
90
  case 'update':
93
- await require('../lib/update')({ check: values.check });
91
+ if (values.target !== undefined) {
92
+ error('`anws update --target` has been removed. Use `anws update` to update all matched targets.');
93
+ process.exit(1);
94
+ }
95
+ if (values.check) {
96
+ error('`anws update --check` has been removed. Use `anws update` directly.');
97
+ process.exit(1);
98
+ }
99
+ await require('../lib/update')();
94
100
  break;
95
101
 
96
102
  default:
package/lib/changelog.js CHANGED
@@ -55,6 +55,61 @@ function compareSemver(a, b) {
55
55
  return 0;
56
56
  }
57
57
 
58
+ function normalizeText(text) {
59
+ return String(text || '').replace(/\r\n/g, '\n');
60
+ }
61
+
62
+ function buildMergedChangeKey(item) {
63
+ const canonicalPath = item.source || item.file;
64
+ const summaryKey = JSON.stringify(item.summary || []);
65
+ const contentKey = item.type === 'modified'
66
+ ? summaryKey
67
+ : `${normalizeText(item.oldContent)}\n<<<ANWS_CHANGE_SPLIT>>>\n${normalizeText(item.newContent)}`;
68
+
69
+ return `${item.type}::${canonicalPath}::${item.resourceId || canonicalPath}::${contentKey}`;
70
+ }
71
+
72
+ function mergeChanges(changes) {
73
+ const grouped = new Map();
74
+
75
+ for (const item of changes) {
76
+ const canonicalPath = item.source || item.file;
77
+ const key = buildMergedChangeKey(item);
78
+ const affectedFile = {
79
+ file: item.file,
80
+ targetId: item.targetId || null,
81
+ targetLabel: item.targetLabel || null
82
+ };
83
+
84
+ if (!grouped.has(key)) {
85
+ grouped.set(key, {
86
+ ...item,
87
+ canonicalPath,
88
+ affectedFiles: [affectedFile]
89
+ });
90
+ continue;
91
+ }
92
+
93
+ const current = grouped.get(key);
94
+ current.affectedFiles.push(affectedFile);
95
+ }
96
+
97
+ return Array.from(grouped.values()).map((item) => ({
98
+ ...item,
99
+ affectedFiles: item.affectedFiles.sort((left, right) => left.file.localeCompare(right.file))
100
+ }));
101
+ }
102
+
103
+ function formatAffectedTargetList(item) {
104
+ const targets = Array.from(new Set(
105
+ item.affectedFiles
106
+ .filter((entry) => entry.targetId && entry.targetLabel)
107
+ .map((entry) => `${entry.targetLabel} (${entry.targetId})`)
108
+ ));
109
+
110
+ return targets.length > 0 ? targets.join(', ') : '无';
111
+ }
112
+
58
113
  function formatFileList(title, items) {
59
114
  const lines = [`### ${title}`];
60
115
  if (items.length === 0) {
@@ -63,7 +118,10 @@ function formatFileList(title, items) {
63
118
  }
64
119
 
65
120
  for (const item of items) {
66
- lines.push(`- \`${item.file}\``);
121
+ lines.push(`- \`${item.canonicalPath}\` — 影响 Targets: ${formatAffectedTargetList(item)}`);
122
+ for (const affectedFile of item.affectedFiles) {
123
+ lines.push(` - \`${affectedFile.file}\``);
124
+ }
67
125
  }
68
126
 
69
127
  return lines.join('\n');
@@ -72,7 +130,10 @@ function formatFileList(title, items) {
72
130
  function formatDetail(item) {
73
131
  if (item.type === 'added') {
74
132
  return [
75
- `### \`${item.file}\``,
133
+ `### \`${item.canonicalPath}\``,
134
+ `- **影响 Targets**: ${formatAffectedTargetList(item)}`,
135
+ '- **影响文件**:',
136
+ ...item.affectedFiles.map((entry) => ` - \`${entry.file}\``),
76
137
  '- **新增文件**',
77
138
  '- **说明**: 该文件在旧版本中不存在,因此无前后逐行对比。'
78
139
  ].join('\n');
@@ -80,7 +141,10 @@ function formatDetail(item) {
80
141
 
81
142
  if (item.type === 'deleted') {
82
143
  return [
83
- `### \`${item.file}\``,
144
+ `### \`${item.canonicalPath}\``,
145
+ `- **影响 Targets**: ${formatAffectedTargetList(item)}`,
146
+ '- **影响文件**:',
147
+ ...item.affectedFiles.map((entry) => ` - \`${entry.file}\``),
84
148
  '- **删除文件**',
85
149
  '- **说明**: 该文件在新版本中不存在,因此无前后逐行对比。'
86
150
  ].join('\n');
@@ -88,13 +152,19 @@ function formatDetail(item) {
88
152
 
89
153
  if (item.summary.length === 0) {
90
154
  return [
91
- `### \`${item.file}\``,
155
+ `### \`${item.canonicalPath}\``,
156
+ `- **影响 Targets**: ${formatAffectedTargetList(item)}`,
157
+ '- **影响文件**:',
158
+ ...item.affectedFiles.map((entry) => ` - \`${entry.file}\``),
92
159
  '- **说明**: 检测到内容变更,但未能提取到摘要。'
93
160
  ].join('\n');
94
161
  }
95
162
 
96
163
  return [
97
- `### \`${item.file}\``,
164
+ `### \`${item.canonicalPath}\``,
165
+ `- **影响 Targets**: ${formatAffectedTargetList(item)}`,
166
+ '- **影响文件**:',
167
+ ...item.affectedFiles.map((entry) => ` - \`${entry.file}\``),
98
168
  '```diff',
99
169
  ...item.summary.flatMap((pair) => [
100
170
  `- [old:${pair.oldLineNumber === null ? '-' : pair.oldLineNumber}] ${pair.oldText}`,
@@ -106,7 +176,8 @@ function formatDetail(item) {
106
176
 
107
177
  async function generateChangelog({ cwd, version, changes, targetSummary = null }) {
108
178
  const changelogDir = await ensureChangelogDir(cwd);
109
- const grouped = groupChanges(changes);
179
+ const mergedChanges = mergeChanges(changes);
180
+ const grouped = groupChanges(mergedChanges);
110
181
  const now = new Date();
111
182
  const timestamp = now.toISOString().replace('T', ' ').slice(0, 19);
112
183
  const filePath = path.join(changelogDir, `v${version}.md`);
@@ -143,7 +214,7 @@ async function generateChangelog({ cwd, version, changes, targetSummary = null }
143
214
  '',
144
215
  '## 内容级变更详情',
145
216
  '',
146
- ...(changes.length > 0 ? changes.map(formatDetail).flatMap((section) => [section, '']) : ['- 无变更', ''])
217
+ ...(mergedChanges.length > 0 ? mergedChanges.map(formatDetail).flatMap((section) => [section, '']) : ['- 无变更', ''])
147
218
  ].join('\n');
148
219
 
149
220
  await fs.writeFile(filePath, content, 'utf8');
package/lib/diff.js CHANGED
@@ -137,6 +137,7 @@ async function collectManagedFileDiffs({
137
137
  ? managedFiles
138
138
  : projectionPlan.flatMap((item) => item.managedFiles || []);
139
139
  const projectionMap = new Map(normalizedProjectionEntries.map((item) => [item.outputPath, item]));
140
+ const fallbackTarget = projectionPlan[0] || null;
140
141
 
141
142
  for (const rel of normalizedManagedFiles) {
142
143
  if (rel === 'AGENTS.md' && !shouldWriteRootAgents) {
@@ -144,6 +145,19 @@ async function collectManagedFileDiffs({
144
145
  }
145
146
 
146
147
  const entry = projectionMap.get(rel);
148
+ const metadata = entry
149
+ ? {
150
+ source: entry.source,
151
+ resourceId: entry.id,
152
+ targetId: entry.targetId,
153
+ targetLabel: entry.targetLabel
154
+ }
155
+ : {
156
+ source: rel,
157
+ resourceId: rel === 'AGENTS.md' ? 'root-agents' : rel,
158
+ targetId: fallbackTarget?.targetId || null,
159
+ targetLabel: fallbackTarget?.targetLabel || null
160
+ };
147
161
  const srcPath = rel === 'AGENTS.md'
148
162
  ? srcAgents
149
163
  : path.join(path.join(__dirname, '..', 'templates'), entry.source);
@@ -160,6 +174,7 @@ async function collectManagedFileDiffs({
160
174
  if (srcExists && !destExists) {
161
175
  results.push({
162
176
  file: rel,
177
+ ...metadata,
163
178
  type: 'added',
164
179
  summary: [],
165
180
  oldContent: '',
@@ -171,6 +186,7 @@ async function collectManagedFileDiffs({
171
186
  if (!srcExists && destExists) {
172
187
  results.push({
173
188
  file: rel,
189
+ ...metadata,
174
190
  type: 'deleted',
175
191
  summary: [],
176
192
  oldContent: await readTextOrEmpty(destPath),
@@ -193,6 +209,7 @@ async function collectManagedFileDiffs({
193
209
 
194
210
  results.push({
195
211
  file: rel,
212
+ ...metadata,
196
213
  type: 'modified',
197
214
  summary: createLineDiff(oldContent, newContent),
198
215
  oldContent,
package/lib/update.js CHANGED
@@ -5,32 +5,33 @@ const path = require('node:path');
5
5
  const { buildProjectionPlan } = require('./manifest');
6
6
  const { getTarget } = require('./adapters');
7
7
  const { planAgentsUpdate, resolveAgentsInstall, printLegacyMigrationWarning, pathExists } = require('./agents');
8
- const { collectManagedFileDiffs, printPreview } = require('./diff');
8
+ const { collectManagedFileDiffs } = require('./diff');
9
9
  const { detectUpgrade, generateChangelog } = require('./changelog');
10
10
  const { writeTargetFiles } = require('./copy');
11
11
  const { createInstallLock, dedupeTargets, detectInstallState, summarizeTargetState, writeInstallLock } = require('./install-state');
12
12
  const { confirm } = require('./prompt');
13
13
  const { ROOT_AGENTS_FILE, resolveCanonicalSource } = require('./resources');
14
- const { success, warn, error, info, fileLine, skippedLine, blank, logo, section } = require('./output');
14
+ const { warn, error, info, fileLine, skippedLine, blank, logo, section } = require('./output');
15
15
 
16
- async function update(options = {}) {
16
+ async function update() {
17
17
  const cwd = process.cwd();
18
- const check = !!options.check;
19
18
  const legacyAgentDir = path.join(cwd, '.agent');
20
19
  const { version } = require(path.join(__dirname, '..', 'package.json'));
21
20
  const installState = await detectInstallState(cwd);
22
21
  const legacyAgentExists = await pathExists(legacyAgentDir);
23
22
  const isLegacyMigration = installState.selectedTargets.length === 0 && legacyAgentExists;
24
- const selectedTargetIds = isLegacyMigration ? ['antigravity'] : installState.selectedTargets;
25
- const targetPlans = buildProjectionPlan(selectedTargetIds);
23
+ const detectedTargetIds = isLegacyMigration ? ['antigravity'] : installState.selectedTargets;
26
24
 
27
- if (selectedTargetIds.length === 0 && !legacyAgentExists) {
25
+ if (detectedTargetIds.length === 0 && !legacyAgentExists) {
28
26
  logo();
29
27
  error('No supported Anws target layout found in current directory.');
30
28
  info('Run `anws init` first to set up the workflow system.');
31
29
  process.exit(1);
32
30
  }
33
31
 
32
+ const targetPlans = buildProjectionPlan(detectedTargetIds);
33
+ const detectedTargetPlans = buildProjectionPlan(detectedTargetIds);
34
+
34
35
  const srcAgents = ROOT_AGENTS_FILE;
35
36
 
36
37
  if (isLegacyMigration) {
@@ -100,41 +101,18 @@ async function update(options = {}) {
100
101
 
101
102
  const changes = targetContexts.flatMap((context) => context.changes);
102
103
 
103
- if (check) {
104
- if (!versionState.needUpgrade) {
105
- if (!isLegacyMigration) {
106
- logo();
107
- blank();
108
- }
109
- info(`Already up to date. Latest recorded version is v${versionState.latestVersion || version}.`);
110
- printTargetSelection(installState, targetContexts.map((context) => context.target));
111
- return;
112
- }
113
- if (!isLegacyMigration) {
114
- logo();
115
- blank();
116
- }
117
- printTargetSelection(installState, targetContexts.map((context) => context.target));
118
- printPreview({
119
- fromVersion: versionState.fromVersion,
120
- toVersion: versionState.toVersion,
121
- changes
122
- });
123
- return;
124
- }
125
-
126
104
  if (!versionState.needUpgrade) {
127
105
  if (!isLegacyMigration) {
128
106
  logo();
129
107
  blank();
130
108
  }
131
109
  printTargetSelection(installState, targetContexts.map((context) => context.target));
132
- if (!check && installState.canRebuildLock && selectedTargetIds.length > 0) {
110
+ if (installState.canRebuildLock && detectedTargetIds.length > 0) {
133
111
  const generatedAt = new Date().toISOString();
134
112
  await writeInstallLock(cwd, createInstallLock({
135
113
  cliVersion: version,
136
114
  generatedAt,
137
- targets: dedupeTargets(targetPlans.map((targetPlan) => summarizeTargetState(targetPlan, version))),
115
+ targets: dedupeTargets(detectedTargetPlans.map((targetPlan) => summarizeTargetState(targetPlan, version))),
138
116
  lastUpdateSummary: {
139
117
  successfulTargets: [],
140
118
  failedTargets: [],
@@ -152,15 +130,7 @@ async function update(options = {}) {
152
130
  blank();
153
131
  }
154
132
 
155
- const confirmed = await askUpdate({
156
- installState,
157
- targets: targetContexts.map((context) => context.target)
158
- });
159
- if (!confirmed) {
160
- blank();
161
- info('Aborted. No files were changed.');
162
- return;
163
- }
133
+ printTargetSelection(installState, targetContexts.map((context) => context.target));
164
134
 
165
135
  const updated = [];
166
136
  const skipped = [];
@@ -217,6 +187,12 @@ async function update(options = {}) {
217
187
  }
218
188
  });
219
189
  const generatedAt = new Date().toISOString();
190
+ const successfulTargetIdSet = new Set(successfulTargets.map((item) => item.targetId));
191
+ const retainedDetectedTargets = installState.canRebuildLock
192
+ ? detectedTargetPlans
193
+ .filter((targetPlan) => !successfulTargetIdSet.has(targetPlan.targetId))
194
+ .map((targetPlan) => summarizeTargetState(targetPlan, version))
195
+ : [];
220
196
  const existingLockTargets = installState.canRebuildLock
221
197
  ? []
222
198
  : (installState.lockResult.lock?.targets || []);
@@ -225,6 +201,7 @@ async function update(options = {}) {
225
201
  generatedAt,
226
202
  targets: dedupeTargets([
227
203
  ...existingLockTargets,
204
+ ...retainedDetectedTargets,
228
205
  ...successfulTargets
229
206
  ]),
230
207
  lastUpdateSummary: {
@@ -251,23 +228,6 @@ async function update(options = {}) {
251
228
  });
252
229
  }
253
230
 
254
- async function askUpdate({ installState, targets }) {
255
- if (global.__ANWS_FORCE_YES) return true;
256
-
257
- if (!process.stdin.isTTY) {
258
- warn('Non-TTY environment detected. Skipping update to avoid accidental overwrites.');
259
- return false;
260
- }
261
-
262
- return confirm({
263
- message: buildUpdateConfirmationMessage(targets),
264
- contextLines: buildUpdateConfirmationContextLines(installState, targets),
265
- confirmLabel: 'Continue',
266
- cancelLabel: 'Cancel',
267
- defaultValue: false
268
- });
269
- }
270
-
271
231
  async function askMigrate() {
272
232
  if (global.__ANWS_FORCE_YES) return true;
273
233
 
@@ -308,23 +268,8 @@ async function maybeDeleteLegacyDir(legacyAgentDir) {
308
268
  return true;
309
269
  }
310
270
 
311
- function buildUpdateConfirmationMessage(targets) {
312
- const labels = targets.map((target) => target.label).filter(Boolean);
313
- if (labels.length === 0) {
314
- return 'This will overwrite all managed files for the detected target layout.';
315
- }
316
- return `This will overwrite all managed files for: ${labels.join(', ')}.`;
317
- }
318
-
319
- function buildUpdateConfirmationContextLines(installState, targets) {
320
- const lines = [
321
- `Matched targets: ${targets.map((target) => `${target.label} (${target.id})`).join(', ') || 'none'}`,
322
- installState.needsFallback ? 'State source: directory scan fallback' : 'State source: install-lock + directory scan'
323
- ];
324
- if (installState.drift.hasDrift) {
325
- lines.push(`State drift detected. Missing on disk: ${installState.drift.missingOnDisk.join(', ') || 'none'}; untracked on disk: ${installState.drift.untrackedOnDisk.join(', ') || 'none'}.`);
326
- }
327
- return lines;
271
+ function buildSelectionModeLine() {
272
+ return 'Selection mode: detected target layout';
328
273
  }
329
274
 
330
275
  function printLegacyMigrationNotice() {
@@ -338,6 +283,7 @@ function printLegacyMigrationNotice() {
338
283
  function printTargetSelection(installState, targets) {
339
284
  blank();
340
285
  section('Target selection', [
286
+ buildSelectionModeLine(),
341
287
  `Matched targets: ${targets.map((target) => `${target.label} (${target.id})`).join(', ') || 'none'}`,
342
288
  installState.needsFallback ? 'State source: directory scan fallback' : 'State source: install-lock + directory scan',
343
289
  ...(installState.drift.hasDrift
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/anws",
3
- "version": "2.0.5",
3
+ "version": "2.1.1",
4
4
  "description": "Anws — A spec-driven workflow framework for AI-assisted development. Empowers prompt engineers to build production-ready software through structured PRD → Architecture → Task decomposition. Works with Claude Code, GitHub Copilot, Cursor, Windsurf, and any tool that reads AGENTS.md.",
5
5
  "keywords": [
6
6
  "anws",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: design-reviewer
3
- description: 使用三维框架(系统设计、运行模拟、工程实现)系统性审查架构和系统设计文档。产出按严重度分级的发现,关联到具体文档段落。
3
+ description: 使用三维框架(系统设计、运行模拟、工程实现)系统性审查架构和系统设计文档,作为 challenge 工作流中的规范契约设计证据层。产出按严重度分级的发现,关联到具体文档段落。
4
4
  ---
5
5
 
6
6
  # 设计审查大师手册
@@ -9,6 +9,8 @@ description: 使用三维框架(系统设计、运行模拟、工程实现)
9
9
  > 对设计严厉,代码才能优雅。"
10
10
 
11
11
  你是**设计审查大师**,负责系统性审查架构和系统设计文档。你的三维框架确保没有任何一类风险被遗漏。
12
+ 在 `/challenge` 工作流中,你的角色是:**为规范契约是否闭合提供设计侧证据**,而不是单独给出脱离上下文的最终裁决。
13
+ 你优先要证明的是:哪些契约在**系统边界、接口、状态、时序、错误路径**上没有闭合。
12
14
 
13
15
  ---
14
16
 
@@ -126,15 +128,28 @@ description: 使用三维框架(系统设计、运行模拟、工程实现)
126
128
  | 工程实现 | — | — | — | — | — |
127
129
  | **合计** | **—** | **—** | **—** | **—** | **—** |
128
130
 
131
+ **高信号结论**: [用 1-3 句概括最值得进入 challenge 主报告的问题]
132
+
129
133
  ---
130
134
 
131
- ### [维度] — [ID]: [标题]
135
+ ### 核心发现清单
132
136
 
133
- **严重度**: Critical / High / Medium / Low
134
- **文档位置**: [精确的文件和章节引用]
137
+ | ID | 维度 | 严重度 | 文档位置 | 发现 | 影响 | 建议 |
138
+ |----|------|--------|----------|------|------|------|
139
+ | DR-01 | 系统设计 | Critical | 02_ARCHITECTURE_OVERVIEW.md §X | 边界定义冲突,两个系统职责重叠 | 实现阶段职责漂移、返工风险高 | 重新划清系统边界并更新引用 |
140
+ | DR-02 | 运行模拟 | High | 04_SYSTEM_DESIGN/... §Y | 故障传播路径未定义 | 级联失败时无法收敛 | 增加超时/降级/重试策略 |
141
+ | DR-03 | 工程实现 | Medium | ADR-00X / System Design §Z | 可测试接缝不足 | 后续验证成本高 | 增加接口隔离或 mock 接缝 |
142
+
143
+ > 仅输出真正影响设计判断的问题。低价值措辞、重复担忧不要进入清单。
144
+
145
+ ---
146
+
147
+ ### Top Findings 详情(仅展开 Critical / High)
135
148
 
136
- **问题**:
137
- [详细描述,引用具体文档内容]
149
+ #### DR-01 [标题]
150
+
151
+ **严重度**: Critical
152
+ **文档位置**: [精确的文件和章节引用]
138
153
 
139
154
  **证据**:
140
155
  - 文档分析: [来自 PRD/Architecture/ADR 的具体内容]
@@ -145,7 +160,7 @@ description: 使用三维框架(系统设计、运行模拟、工程实现)
145
160
  - [不修复会发生什么]
146
161
 
147
162
  **建议**:
148
- [修复方式,可提供多个选项]
163
+ - [最小修复方向]
149
164
  ```
150
165
 
151
166
  ---
@@ -537,14 +537,31 @@ classDiagram
537
537
  - **RBAC (Role-Based Access Control)**: 基于角色的访问控制
538
538
  - **p95**: 95th percentile,95%的请求响应时间小于该值
539
539
 
540
- ### 14.2 References (参考资料)
540
+ ### 14.2 Optional Skills & Reference Resources (可选 Skills 与参考资源)
541
+ >
542
+ > 本节用于记录在设计过程中实际参考过的 skill、组件库、方法论或外部资料。
543
+ > 这些内容是辅助输入,不是系统事实来源;最终方案仍以本项目的 PRD、ADR、Architecture Overview 和本文档自身为准。
544
+ >
545
+ > **记录建议**:
546
+ > - 写明资源名称
547
+ > - 写明它帮助了哪个设计决策
548
+ > - 写明最终采纳了什么,舍弃了什么
549
+ >
550
+ > **示例(前端系统)**:
551
+ > - `vercel-react-best-practices`: 用于校验 React 组件边界、渲染策略、性能优化建议
552
+ > - `frontend-design`: 用于参考排版、配色、层级和动效方向
553
+ > - `shadcn/ui`: 用于基础组件模式参考
554
+ > - `Aceternity UI`: 用于展示型区块和交互动效灵感
555
+ > - `Magic UI`: 用于 Tailwind-first 的视觉与动画参考
556
+
557
+ ### 14.3 References (参考资料)
541
558
  - [FastAPI Documentation](https://fastapi.tiangolo.com/)
542
559
  - [PostgreSQL Best Practices](https://wiki.postgresql.org/wiki/Don%27t_Do_This)
543
560
  - [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
544
561
  - [Architecture Overview](../02_ARCHITECTURE_OVERVIEW.md)
545
562
  - [ADR001: Tech Stack](../03_ADR/ADR001_TECH_STACK.md)
546
563
 
547
- ### 14.3 Change Log (变更日志)
564
+ ### 14.4 Change Log (变更日志)
548
565
 
549
566
  | Version | Date | Changes | Author |
550
567
  | ------- | ---------- | -------- | ------ |
@@ -18,8 +18,10 @@ description: 使用WBS方法将系统设计文档分解为层次化任务。支
18
18
  2. **加载必需文档**:读取 Architecture Overview + PRD
19
19
  3. **加载可选文档**:扫描 ADR 目录 + System Design 目录
20
20
  4. **缺失检查**:必需文档缺失则报错退出
21
- 5. **执行拆解**:按 WBS 方法拆解任务
22
- 6. **输出**:保存到 `.anws/v{N}/05_TASKS.md`
21
+ 5. **加载测试约束**:如果 Workflow 或 ADR 提供测试策略、质量门禁、Sprint 边界,必须一并纳入任务生成输入
22
+ 6. **执行拆解**:按 WBS 方法拆解任务
23
+ 7. **应用验证类型选择逻辑**:为每个任务分配“最轻但足够”的验证类型,避免默认升级为 E2E
24
+ 8. **输出**:保存到 `.anws/v{N}/05_TASKS.md`
23
25
 
24
26
  ---
25
27
 
@@ -33,6 +35,13 @@ description: 使用WBS方法将系统设计文档分解为层次化任务。支
33
35
  > 3. **可验证** - 每个Task有明确的Done When标准
34
36
  > 4. **可追溯** - 每个Task关联PRD需求 [REQ-XXX]
35
37
 
38
+ > [!IMPORTANT]
39
+ > **测试规划附加原则**:
40
+ > - 优先选择**最轻但足够**的验证类型
41
+ > - 如 Workflow / ADR 已声明测试策略,必须优先遵循,不得自行改重
42
+ > - **冒烟测试默认仅用于 `INT-S{N}` 或极少数里程碑任务**
43
+ > - **回归测试仅在已有关键能力可能被破坏时生成**,不是所有任务的默认要求
44
+
36
45
  ❌ **错误做法**:
37
46
  - 平铺任务列表(无层次)
38
47
  - 任务过大(如"实现整个后端")
@@ -114,6 +123,7 @@ description: 使用WBS方法将系统设计文档分解为层次化任务。支
114
123
  - **验收标准**:
115
124
  - [ ] Done When 1
116
125
  - [ ] Done When 2
126
+ - **验证类型**: 单元测试 | 集成测试 | E2E测试 | 冒烟测试 | 回归测试 | 手动验证 | 编译检查 | Lint检查
117
127
  - **验证说明**: 如何确认任务完成 (检查什么,如何确认)
118
128
  - **估时**: 预估工时(如: 2h, 1d, 1w)
119
129
  - **依赖**: T{X}.{Y}.{Z} (依赖的Task ID)
@@ -130,11 +140,39 @@ description: 使用WBS方法将系统设计文档分解为层次化任务。支
130
140
  - [ ] `npm run dev` 正常启动
131
141
  - [ ] 页面显示"Hello World"
132
142
  - [ ] TypeScript类型检查通过
143
+ - **验证类型**: 编译检查
133
144
  - **估时**: 2h
134
145
  - **依赖**: 无
135
146
  - **优先级**: P0
136
147
  ```
137
148
 
149
+ ### 验证类型选择逻辑
150
+
151
+ > [!IMPORTANT]
152
+ > **如果 Workflow 未给出更具体约束,按以下默认顺序决策:**
153
+
154
+ 1. **局部逻辑 / 纯算法 / 数据变换** → 单元测试
155
+ 2. **跨模块 / 接口 / 数据库 / 多服务协作** → 集成测试
156
+ 3. **直接面向终端用户的关键路径** → E2E测试 或 手动验证
157
+ 4. **Sprint 退出标准 / 里程碑 gate** → 冒烟测试
158
+ 5. **修改可能影响已完成关键能力** → 回归测试
159
+ 6. **配置、脚手架、基础设施** → 编译检查 / Lint检查 / 手动验证
160
+
161
+ **选择细则**:
162
+ - 不要因为任务“看起来重要”就默认选择 E2E测试
163
+ - 如果集成测试足以证明任务完成,就不要升级为 E2E测试
164
+ - 如果只是里程碑 readiness 检查,优先使用少量冒烟测试,而不是新建大量 E2E任务
165
+ - 如果只是验证旧能力未被破坏,优先复用已有测试集合作为回归测试
166
+
167
+ ### Sprint 与冒烟测试绑定规则
168
+
169
+ > [!IMPORTANT]
170
+ > **只有在 Workflow 已提供 Sprint 路线图 / INT 任务语义时,才应生成里程碑级冒烟测试。**
171
+
172
+ - 如果上游 Workflow 已定义 `INT-S{N}`,则将冒烟测试优先绑定到这些 INT 任务
173
+ - 不要为每个普通 Level 3 开发任务单独生成冒烟测试
174
+ - 若没有明确 Sprint / 里程碑边界,则优先退回单元测试、集成测试、手动验证,而不是滥造冒烟任务
175
+
138
176
  ### 接口追溯规则
139
177
 
140
178
  > [!IMPORTANT]