@zhuan-ai/zhuanspec 2.17.14 → 2.19.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/dist/cli/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  import { createRequire } from 'module';
3
3
  import ora from 'ora';
4
4
  import path from 'path';
@@ -104,8 +104,9 @@ const registerTechDesignCommand = (commandName, deprecatedAlias = false) => prog
104
104
  : 'Prepare technical design generation request (techDesign phase, independent before proposal by default)')
105
105
  .option('--change <change-id>', 'Optionally bind generated design source to a change')
106
106
  .option('--desc <text>', 'Requirement description used for technical design generation')
107
- .option('--dashen-page-id <id>', 'Dashen pageId used as requirement source')
108
- .option('--dashen-url <url>', 'Dashen document URL used as requirement source')
107
+ .option('--feishu-url <url>', 'Feishu document URL used as requirement source')
108
+ .addOption(new Option('--dashen-page-id <id>', 'Legacy pageId used as requirement source').hideHelp())
109
+ .addOption(new Option('--dashen-url <url>', 'Legacy document URL used as requirement source').hideHelp())
109
110
  .option('--output <path>', 'Output path of generated request markdown (default: doc/*技术方案生成请求.md)')
110
111
  .option('--init-template', 'Generate empty design.md template inside a change (legacy mode)')
111
112
  .option('--force', 'Overwrite existing output file or bound design.md')
@@ -117,7 +118,11 @@ const registerTechDesignCommand = (commandName, deprecatedAlias = false) => prog
117
118
  console.error('Warning: "zhuanspec design" is deprecated. Please use "zhuanspec techDesign".');
118
119
  }
119
120
  const designCommand = new DesignCommand();
120
- await designCommand.execute(changeName, options);
121
+ await designCommand.execute(changeName, {
122
+ ...options,
123
+ legacyPageId: options?.dashenPageId,
124
+ legacyUrl: options?.dashenUrl,
125
+ });
121
126
  }
122
127
  catch (error) {
123
128
  console.log();
@@ -13,7 +13,7 @@ import { createChange, validateChangeName } from '../utils/change-utils.js';
13
13
  import { discoverSkills } from '../core/skill-discovery.js';
14
14
  import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getProposeChangeSkillTemplate, getExploreSkillTemplate, getVerifySkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxProposeCommandTemplate, getOpsxExploreCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
15
15
  import { FileSystemUtils } from '../utils/file-system.js';
16
- import { parseTasks, generateExecutionPlan, renderExecutionPlanXml, renderMermaidDiagram, hasDependsAnnotations, } from '../core/task-graph/index.js';
16
+ import { parseTasks, generateExecutionPlan, renderExecutionPlanXml, hasDependsAnnotations, } from '../core/task-graph/index.js';
17
17
  const DEFAULT_SCHEMA = 'spec-driven';
18
18
  /**
19
19
  * Checks if color output is disabled via NO_COLOR env or --no-color flag.
@@ -427,26 +427,6 @@ async function generateApplyInstructions(projectRoot, changeName, schemaName) {
427
427
  if (hasDependencies && !allTasksComplete) {
428
428
  const executionPlan = generateExecutionPlan(parsedTasks);
429
429
  executionPlanXml = renderExecutionPlanXml(executionPlan);
430
- // Generate Mermaid diagram and embed in tasks.md
431
- if (executionPlan && !executionPlan.hasCycle) {
432
- const mermaidSection = renderMermaidDiagram(executionPlan, parsedTasks);
433
- if (mermaidSection) {
434
- // Read current tasks.md content
435
- let currentTasksContent = tasksContent;
436
- // Check if tasks.md already has Workflow Diagram section
437
- const workflowDiagramRegex = /## Workflow Diagram[\s\S]*?(?=\n## |$)/;
438
- if (workflowDiagramRegex.test(currentTasksContent)) {
439
- // Replace existing Workflow Diagram section
440
- currentTasksContent = currentTasksContent.replace(workflowDiagramRegex, mermaidSection);
441
- }
442
- else {
443
- // Append to file end
444
- currentTasksContent = currentTasksContent.trimEnd() + '\n\n' + mermaidSection;
445
- }
446
- // Write back to tasks.md
447
- fs.writeFileSync(tracksPath, currentTasksContent, 'utf-8');
448
- }
449
- }
450
430
  }
451
431
  }
452
432
  }
@@ -475,17 +455,15 @@ async function generateApplyInstructions(projectRoot, changeName, schemaName) {
475
455
  }
476
456
  else if (tracksFile && remaining === 0 && total > 0) {
477
457
  state = 'all_done';
478
- instruction = `All tasks are complete. Review should auto-run once after apply completion.
458
+ instruction = `All tasks are complete. Apply is finished.
479
459
 
480
460
  **Required Next Steps**
481
461
 
482
- 1. Verify auto review report exists (\`metrics/review.json\`, \`metrics/tests.json\`)
483
- 2. Ensure unit tests pass in the review report
484
- 3. Ensure critical issues = 0
485
- 4. If auto review failed/missing, run \`zhuanspec review ${changeName}\` manually
486
- 5. Then continue to \`zhuanspec archive ${changeName}\`
462
+ 1. Run \`zhuanspec review ${changeName}\` to verify implementation quality
463
+ 2. Fix any review findings if needed
464
+ 3. Continue to \`zhuanspec archive ${changeName}\` after review passes
487
465
 
488
- Archive may be blocked if review/tests are missing or failed.`;
466
+ Archive may be blocked if review results are missing or failed.`;
489
467
  }
490
468
  else if (!tracksFile) {
491
469
  // No tracking file (e.g., TDD schema) - ready to apply
@@ -588,16 +566,13 @@ function printApplyInstructionsText(instructions) {
588
566
  // Code review prompt for all_done state
589
567
  if (state === 'all_done') {
590
568
  console.log();
591
- console.log('### 📋 Code Review (Auto + Required)');
569
+ console.log('### Next Step');
592
570
  console.log();
593
571
  console.log('All tasks are complete. You should now:');
594
572
  console.log();
595
- console.log('1. Auto review is triggered once when all apply tasks complete');
596
- console.log('2. Confirm `metrics/review.json` and `metrics/tests.json` exist');
597
- console.log('3. Ensure review report is PASS with no critical issues');
598
- console.log('4. Ensure unit tests pass');
599
- console.log('5. If auto review failed/missing, run `zhuanspec review <change-id>` manually');
600
- console.log('6. Then proceed to archive workflow');
573
+ console.log('1. Run `zhuanspec review <change-id>`');
574
+ console.log('2. Fix review findings if needed');
575
+ console.log('3. Then proceed to archive workflow');
601
576
  }
602
577
  }
603
578
  async function templatesCommand(options) {
@@ -4,8 +4,9 @@ export declare class DesignCommand {
4
4
  initTemplate?: boolean;
5
5
  change?: string;
6
6
  desc?: string;
7
- dashenPageId?: string;
8
- dashenUrl?: string;
7
+ feishuUrl?: string;
8
+ legacyPageId?: string;
9
+ legacyUrl?: string;
9
10
  output?: string;
10
11
  validate?: boolean;
11
12
  autoFix?: boolean;
@@ -49,8 +49,9 @@ export class DesignCommand {
49
49
  if (options?.createChange && !boundChange) {
50
50
  const designContext = await this.resolveDesignContext({
51
51
  desc: options?.desc,
52
- dashenPageId: options?.dashenPageId,
53
- dashenUrl: options?.dashenUrl,
52
+ feishuUrl: options?.feishuUrl,
53
+ legacyPageId: options?.legacyPageId,
54
+ legacyUrl: options?.legacyUrl,
54
55
  });
55
56
  const newChangeName = await this.generateChangeName(designContext);
56
57
  await this.createTechDesignChange(newChangeName, designContext);
@@ -76,11 +77,12 @@ export class DesignCommand {
76
77
  }
77
78
  const designContext = await this.resolveDesignContext({
78
79
  desc: options?.desc,
79
- dashenPageId: options?.dashenPageId,
80
- dashenUrl: options?.dashenUrl,
80
+ feishuUrl: options?.feishuUrl,
81
+ legacyPageId: options?.legacyPageId,
82
+ legacyUrl: options?.legacyUrl,
81
83
  });
82
- if (!designContext.desc && !designContext.dashenPageId && !designContext.dashenUrl) {
83
- throw new Error('Missing design source. Provide --desc / --dashen-page-id / --dashen-url to generate technical design.');
84
+ if (!designContext.desc && !designContext.feishuUrl && !designContext.legacyPageId && !designContext.legacyUrl) {
85
+ throw new Error('Missing design source. Provide --desc or --feishu-url to generate technical design.');
84
86
  }
85
87
  const outputPath = await this.resolveOutputPath(options?.output, boundChange);
86
88
  const exists = await FileSystemUtils.fileExists(outputPath);
@@ -112,15 +114,16 @@ export class DesignCommand {
112
114
  async resolveDesignContext(input) {
113
115
  const result = {
114
116
  desc: input.desc?.trim() ?? '',
115
- dashenPageId: input.dashenPageId?.trim() ?? '',
116
- dashenUrl: input.dashenUrl?.trim() ?? '',
117
+ feishuUrl: input.feishuUrl?.trim() ?? '',
118
+ legacyPageId: input.legacyPageId?.trim() ?? '',
119
+ legacyUrl: input.legacyUrl?.trim() ?? '',
117
120
  };
118
- if (result.desc || result.dashenPageId || result.dashenUrl || !process.stdin.isTTY) {
121
+ if (result.desc || result.feishuUrl || result.legacyPageId || result.legacyUrl || !process.stdin.isTTY) {
119
122
  return result;
120
123
  }
121
124
  const { input: askInput } = await import('@inquirer/prompts');
122
125
  const source = await askInput({
123
- message: '请输入需求描述(或留空后继续输入 Dashen 信息)',
126
+ message: '请输入需求描述或飞书文档 URL(飞书文档将使用飞书 CLI 读取)',
124
127
  default: '',
125
128
  });
126
129
  result.desc = source.trim();
@@ -147,12 +150,12 @@ export class DesignCommand {
147
150
  '',
148
151
  '## 输入',
149
152
  `- 需求描述: ${context.desc || '(未提供)'}`,
150
- `- Dashen Page ID: ${context.dashenPageId || '(未提供)'}`,
151
- `- Dashen URL: ${context.dashenUrl || '(未提供)'}`,
153
+ `- 飞书文档 URL: ${context.feishuUrl || context.legacyUrl || '(未提供)'}`,
154
+ '- 飞书读取方式: 使用飞书 CLI `lark-cli` 读取正文',
152
155
  `- 绑定变更: ${boundChange || '(无,独立于提案阶段)'}`,
153
156
  '',
154
157
  '## 生成要求',
155
- '- 使用 Dashen 模板优先,本地模板兜底',
158
+ '- 使用飞书文档内容作为需求来源,本地输入兜底',
156
159
  '- 输出完整技术方案(含 Mermaid)',
157
160
  '- 覆盖需求功能点并附带校验结果与修复记录',
158
161
  '',
@@ -346,12 +349,17 @@ export class DesignCommand {
346
349
  if (context.desc) {
347
350
  base = context.desc;
348
351
  }
349
- else if (context.dashenPageId) {
350
- base = `tech-design-${context.dashenPageId}`;
352
+ else if (context.feishuUrl) {
353
+ const docMatch = context.feishuUrl.match(/(?:docx|wiki)\/([^/?#]+)/);
354
+ const storyMatch = context.feishuUrl.match(/story\/detail\/([^/?#]+)/);
355
+ base = `tech-design-${docMatch?.[1] || storyMatch?.[1] || Date.now()}`;
351
356
  }
352
- else if (context.dashenUrl) {
357
+ else if (context.legacyPageId) {
358
+ base = `tech-design-${context.legacyPageId}`;
359
+ }
360
+ else if (context.legacyUrl) {
353
361
  // Extract page ID from URL if possible
354
- const match = context.dashenUrl.match(/pages\/(\d+)/);
362
+ const match = context.legacyUrl.match(/pages\/(\d+)/);
355
363
  base = match ? `tech-design-${match[1]}` : `tech-design-${Date.now()}`;
356
364
  }
357
365
  else {
@@ -391,9 +399,9 @@ export class DesignCommand {
391
399
  // 3. Create techDesign/ directory and tech-spec.md placeholder
392
400
  const techDesignDir = path.join(changeDir, 'techDesign');
393
401
  await FileSystemUtils.createDirectory(techDesignDir);
394
- await FileSystemUtils.writeFile(path.join(techDesignDir, 'tech-spec.md'), `# Tech Spec\n\ncreated: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n\n> This file will be populated by the techDesign skill.\n`);
402
+ await FileSystemUtils.writeFile(path.join(techDesignDir, 'tech-spec.md'), `# Tech Spec\n\ncreated: ${new Date().toISOString()}\nsource: ${context.desc || context.feishuUrl || context.legacyPageId || 'manual'}\n\n> This file will be populated by the techDesign skill.\n`);
395
403
  // 3b. Also create .tech-design marker for backward compatibility
396
- await FileSystemUtils.writeFile(path.join(changeDir, '.tech-design'), `created: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n`);
404
+ await FileSystemUtils.writeFile(path.join(changeDir, '.tech-design'), `created: ${new Date().toISOString()}\nsource: ${context.desc || context.feishuUrl || context.legacyPageId || 'manual'}\n`);
397
405
  // 4. Create design.md skeleton
398
406
  await FileSystemUtils.writeFile(path.join(changeDir, 'design.md'), DEFAULT_DESIGN_TEMPLATE);
399
407
  // 5. Create doc/ request file (keep compatibility)
@@ -86,13 +86,8 @@ export const COMMAND_REGISTRY = [
86
86
  takesValue: true,
87
87
  },
88
88
  {
89
- name: 'dashen-page-id',
90
- description: 'Dashen pageId used as requirement source',
91
- takesValue: true,
92
- },
93
- {
94
- name: 'dashen-url',
95
- description: 'Dashen document URL used as requirement source',
89
+ name: 'feishu-url',
90
+ description: 'Feishu document URL used as requirement source',
96
91
  takesValue: true,
97
92
  },
98
93
  {
@@ -14,7 +14,7 @@ argument-hint: feature description or request
14
14
  ---`,
15
15
  design: `---
16
16
  description: 在 proposal 之前独立生成技术设计请求文档。
17
- argument-hint: dashen-page-id or description
17
+ argument-hint: feishu-url or description
18
18
  ---`,
19
19
  apply: `---
20
20
  description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
@@ -14,7 +14,7 @@ argument-hint: feature description or request
14
14
  ---`,
15
15
  design: `---
16
16
  description: "在 proposal 之前独立生成技术设计请求文档。"
17
- argument-hint: dashen-page-id or description
17
+ argument-hint: feishu-url or description
18
18
  ---`,
19
19
  apply: `---
20
20
  description: "实施已批准的 ZhuanSpec 变更并保持任务同步。"
@@ -14,7 +14,7 @@ argument-hint: request or feature description
14
14
  ---`,
15
15
  design: `---
16
16
  description: 在 proposal 之前独立生成技术设计请求文档。
17
- argument-hint: dashen-page-id or description
17
+ argument-hint: feishu-url or description
18
18
  ---`,
19
19
  apply: `---
20
20
  description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
@@ -200,8 +200,7 @@ async function runInitHook(_options) {
200
200
  const versionBanner = await checkVersionUpdate();
201
201
  // Build system message — intentionally minimal. Only the version banner
202
202
  // and active-change phase line are surfaced. Per-phase guidance lives in
203
- // the corresponding Skill / slash-command template (e.g. zhuanspec-apply
204
- // carries the subagent spawn authorization).
203
+ // the corresponding Skill / slash-command template.
205
204
  let systemMessage = '';
206
205
  if (versionBanner) {
207
206
  systemMessage += `${versionBanner}\n`;
@@ -220,7 +219,7 @@ async function runInitHook(_options) {
220
219
  systemMessage += ` (run /zhuanspec:proposal to continue)`;
221
220
  }
222
221
  else if (phase === 'apply') {
223
- systemMessage += ` (run zhuanspec-apply skill to execute Waves)`;
222
+ systemMessage += ` (run zhuanspec-apply to implement pending tasks)`;
224
223
  }
225
224
  systemMessage += `\n`;
226
225
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Actions after Apply phase completes:
5
5
  * 1. Reverse Sync deviation detection
6
- * 2. Report status (user manually triggers review in a new session)
6
+ * 2. Report status (user manually triggers review when ready)
7
7
  */
8
8
  interface PostApplyOptions {
9
9
  json?: boolean;
@@ -16,14 +16,17 @@ interface HookOutput {
16
16
  reviewTriggered?: boolean;
17
17
  deviationDetected?: boolean;
18
18
  deviationFiles?: string[];
19
+ featureOmissionDetected?: boolean;
20
+ uncoveredFeatures?: string[];
21
+ missingScenarios?: string[];
22
+ partialScenarios?: string[];
19
23
  };
20
24
  }
21
25
  export declare function postApplyHook(options: PostApplyOptions): Promise<void>;
22
26
  /**
23
27
  * Execute post-apply actions: deviation detection only.
24
- * Auto review trigger has been removed to ensure Stop hook fires correctly
25
- * and apply phaseBaseline is properly marked.
26
- * Users should manually run `zhuanspec review <change-id>` in a new session.
28
+ * Auto review trigger has been removed so apply stays focused on implementation.
29
+ * Users should manually run `zhuanspec review <change-id>` when they want full validation.
27
30
  */
28
31
  export declare function executePostApply(changeDir: string, repoRoot: string): Promise<HookOutput>;
29
32
  export {};
@@ -3,11 +3,12 @@
3
3
  *
4
4
  * Actions after Apply phase completes:
5
5
  * 1. Reverse Sync deviation detection
6
- * 2. Report status (user manually triggers review in a new session)
6
+ * 2. Report status (user manually triggers review when ready)
7
7
  */
8
8
  import path from 'path';
9
9
  import { FileSystemUtils } from '../../utils/file-system.js';
10
10
  import { GitRepoDetector } from '../../utils/git-repo-detector.js';
11
+ import { promises as fs } from 'fs';
11
12
  import { getBeijingTime, atomicWriteJson, recoverProgressJsonForWrite, } from './record-progress.js';
12
13
  import { ensureAccuracySnapshot } from '../metrics/code-accuracy.js';
13
14
  import { detectHookHost, sanitizeCodexEnvelope } from '../../utils/hook-host.js';
@@ -39,11 +40,36 @@ async function runPostApplyHook(changeId) {
39
40
  }
40
41
  /**
41
42
  * Execute post-apply actions: deviation detection only.
42
- * Auto review trigger has been removed to ensure Stop hook fires correctly
43
- * and apply phaseBaseline is properly marked.
44
- * Users should manually run `zhuanspec review <change-id>` in a new session.
43
+ * Auto review trigger has been removed so apply stays focused on implementation.
44
+ * Users should manually run `zhuanspec review <change-id>` when they want full validation.
45
45
  */
46
46
  export async function executePostApply(changeDir, repoRoot) {
47
+ // 0. Feature-omission gate(功能点遗漏闸口):检测「范围内但未实现」的 Scenario/功能点
48
+ // 对应问题:PRD 已有功能点被静默漏实现,Review 未拦住。此处提前拦截,afterAction=pause。
49
+ const omissionResult = await detectFeatureOmission(changeDir);
50
+ if (omissionResult.detected) {
51
+ const parts = [];
52
+ if (omissionResult.uncoveredFeatures.length > 0) {
53
+ parts.push(`功能点遗漏 ${omissionResult.uncoveredFeatures.length} 个: ${omissionResult.uncoveredFeatures.join(', ')}`);
54
+ }
55
+ if (omissionResult.missingScenarios.length > 0) {
56
+ parts.push(`未实现 Scenario ${omissionResult.missingScenarios.length} 个`);
57
+ }
58
+ if (omissionResult.partialScenarios.length > 0) {
59
+ parts.push(`疑似未实现(partial) Scenario ${omissionResult.partialScenarios.length} 个`);
60
+ }
61
+ return {
62
+ continue: false,
63
+ systemMessage: `Post-apply BLOCKED: 检测到范围内功能点/Scenario 未落地(${parts.join(';')})。请补齐缺失的代码实现(禁止改删 spec/技术方案/PRD),再重新进入 Review。`,
64
+ hookSpecificOutput: {
65
+ reviewTriggered: false,
66
+ featureOmissionDetected: true,
67
+ uncoveredFeatures: omissionResult.uncoveredFeatures,
68
+ missingScenarios: omissionResult.missingScenarios,
69
+ partialScenarios: omissionResult.partialScenarios,
70
+ },
71
+ };
72
+ }
47
73
  // 1. Reverse Sync deviation detection
48
74
  const deviationResult = await detectDeviation(changeDir, repoRoot);
49
75
  // 2. Report deviation status (no auto review trigger)
@@ -57,7 +83,7 @@ export async function executePostApply(changeDir, repoRoot) {
57
83
  }
58
84
  return {
59
85
  continue: true,
60
- systemMessage: 'Post-apply: No deviation detected. Please run `zhuanspec review` in a new session to enter Review phase.',
86
+ systemMessage: 'Post-apply: No deviation detected. Please run `zhuanspec review` when you are ready to validate the change.',
61
87
  hookSpecificOutput: {
62
88
  reviewTriggered: false,
63
89
  deviationDetected: false,
@@ -75,6 +101,49 @@ export async function executePostApply(changeDir, repoRoot) {
75
101
  },
76
102
  };
77
103
  }
104
+ /**
105
+ * 功能点遗漏检测:读取 apply 阶段 spec-code 一致性报告,识别「范围内但未实现」的遗漏。
106
+ *
107
+ * 数据来源优先级:
108
+ * 1. review/spec-consistency-result.json(consistency-check Skill 产出,含 uncoveredScenarios/partialScenarios/featureTrace)
109
+ * 2. 若报告不存在 → 视为未检测(detected=false),不阻断(交由 review 阶段兜底)
110
+ *
111
+ * 判定为遗漏(detected=true)的条件(任一命中):
112
+ * - featureTrace.uncoveredFeatures 非空(技术方案功能点 F 无落地 Scenario)
113
+ * - uncoveredScenarios 中存在 status=missing 的项
114
+ * - partialScenarios 非空(仅命中辅助关键词,疑似未实现)
115
+ */
116
+ async function detectFeatureOmission(changeDir) {
117
+ const empty = {
118
+ detected: false,
119
+ uncoveredFeatures: [],
120
+ missingScenarios: [],
121
+ partialScenarios: [],
122
+ };
123
+ const reportPath = path.join(changeDir, 'review', 'spec-consistency-result.json');
124
+ let report;
125
+ try {
126
+ const raw = await fs.readFile(reportPath, 'utf-8');
127
+ report = JSON.parse(raw);
128
+ }
129
+ catch {
130
+ // 报告不存在或解析失败 → 不阻断,交由 review 阶段兜底
131
+ return empty;
132
+ }
133
+ const uncoveredFeatures = Array.isArray(report.featureTrace?.uncoveredFeatures)
134
+ ? report.featureTrace.uncoveredFeatures.filter((f) => typeof f === 'string')
135
+ : [];
136
+ const missingScenarios = Array.isArray(report.uncoveredScenarios)
137
+ ? report.uncoveredScenarios
138
+ .filter(s => s?.status === 'missing')
139
+ .map(s => s.scenario || '(未命名 Scenario)')
140
+ : [];
141
+ const partialScenarios = Array.isArray(report.partialScenarios)
142
+ ? report.partialScenarios.map(s => s?.scenario || '(未命名 Scenario)')
143
+ : [];
144
+ const detected = uncoveredFeatures.length > 0 || missingScenarios.length > 0 || partialScenarios.length > 0;
145
+ return { detected, uncoveredFeatures, missingScenarios, partialScenarios };
146
+ }
78
147
  /**
79
148
  * Detect deviation by comparing proposal scope with actual git diff
80
149
  */
@@ -81,6 +81,25 @@ interface SpecConsistencyOutput {
81
81
  scenarioId: string;
82
82
  description: string;
83
83
  }>;
84
+ /**
85
+ * 仅命中辅助关键词(类名/表名)、未命中核心关键词的疑似未实现项。
86
+ * 非空视为未通过——必须澄清为 covered 或补齐代码,禁止改删 spec。
87
+ */
88
+ partialScenarios?: Array<{
89
+ scenario?: string;
90
+ sourceFeature?: string;
91
+ matchedKeyword?: string;
92
+ warning?: string;
93
+ }>;
94
+ /**
95
+ * 功能点 F 编号全链核对结果(以 feature-manifest.json 的 F 编号为分母)。
96
+ * uncoveredFeatures 非空 = 技术方案功能点无落地 Scenario,属功能点遗漏。
97
+ */
98
+ featureTrace?: {
99
+ totalFeatures?: number;
100
+ coveredFeatures?: number;
101
+ uncoveredFeatures?: string[];
102
+ };
84
103
  mapping: Array<{
85
104
  requirementId: string;
86
105
  scenarioId: string;
@@ -261,9 +261,17 @@ export async function specConsistencyResultCheck(changeId, skillOutput) {
261
261
  };
262
262
  }
263
263
  const loopCount = output.loopCount || 0;
264
- // Check consistency coverage
264
+ // Check consistency coverage —— 三类缺口任一命中即视为未通过(层4 终检):
265
+ // 1. missing:核心与辅助关键词均未命中的 Scenario
266
+ // 2. partial:仅命中辅助关键词(类名/表名)的疑似未实现项
267
+ // 3. uncoveredFeatures:feature-manifest 中无落地 Scenario 的功能点 F
268
+ const partialScenarios = output.partialScenarios || [];
269
+ const uncoveredFeatures = output.featureTrace?.uncoveredFeatures || [];
265
270
  const hasUncovered = output.uncoveredScenarios.length > 0;
266
- if (hasUncovered) {
271
+ const hasPartial = partialScenarios.length > 0;
272
+ const hasUncoveredFeatures = uncoveredFeatures.length > 0;
273
+ const hasGap = hasUncovered || hasPartial || hasUncoveredFeatures;
274
+ if (hasGap) {
267
275
  if (loopCount > MAX_LOOP_COUNT) {
268
276
  return {
269
277
  pass: false,
@@ -273,6 +281,8 @@ export async function specConsistencyResultCheck(changeId, skillOutput) {
273
281
  consistencyRate: output.consistencyRate,
274
282
  totalScenarios: output.totalScenarios,
275
283
  coveredScenarios: output.coveredScenarios,
284
+ partialScenarios,
285
+ uncoveredFeatures,
276
286
  loopExceeded: true,
277
287
  },
278
288
  nextAction: 'stop',
@@ -286,12 +296,14 @@ export async function specConsistencyResultCheck(changeId, skillOutput) {
286
296
  metrics: {
287
297
  consistencyRate: output.consistencyRate,
288
298
  uncoveredScenarios: output.uncoveredScenarios,
299
+ partialScenarios,
300
+ uncoveredFeatures,
289
301
  },
290
302
  nextAction: 'fix',
291
303
  fixPrompt: formatConsistencyFixPrompt(output, loopCount),
292
304
  };
293
305
  }
294
- // PASS: All scenarios covered
306
+ // PASS: All scenarios covered, no partial, no uncovered features
295
307
  return {
296
308
  pass: true,
297
309
  needFix: false,
@@ -301,6 +313,7 @@ export async function specConsistencyResultCheck(changeId, skillOutput) {
301
313
  totalRequirements: output.totalRequirements,
302
314
  totalScenarios: output.totalScenarios,
303
315
  coveredScenarios: output.coveredScenarios,
316
+ featureTrace: output.featureTrace,
304
317
  mapping: output.mapping,
305
318
  },
306
319
  nextAction: 'continue',
@@ -709,20 +722,33 @@ function formatMissingUnitTestEvidencePrompt(loopCount) {
709
722
  `;
710
723
  }
711
724
  function formatConsistencyFixPrompt(output, loopCount) {
712
- const uncoveredList = output.uncoveredScenarios.map(s => `- ${s.requirementId}/${s.scenarioId}: ${s.description}`).join('\n');
725
+ const uncoveredList = output.uncoveredScenarios.map(s => `- [missing] ${s.requirementId}/${s.scenarioId}: ${s.description}`).join('\n');
726
+ const partialScenarios = output.partialScenarios || [];
727
+ const partialList = partialScenarios.map(s => `- [partial] ${s.scenario || '(未命名)'}${s.sourceFeature ? ` [${s.sourceFeature}]` : ''}: ${s.warning || '仅命中辅助关键词,疑似未实现'}`).join('\n');
728
+ const uncoveredFeatures = output.featureTrace?.uncoveredFeatures || [];
729
+ const featureList = uncoveredFeatures.map(f => `- [feature] ${f}: 该功能点无落地 Scenario,属功能点遗漏`).join('\n');
730
+ const sections = [];
731
+ if (output.uncoveredScenarios.length > 0) {
732
+ sections.push(`未覆盖 Scenario(${output.uncoveredScenarios.length} 个):\n${uncoveredList}`);
733
+ }
734
+ if (partialScenarios.length > 0) {
735
+ sections.push(`疑似未实现 partial(${partialScenarios.length} 个,仅命中类名/表名等辅助关键词):\n${partialList}`);
736
+ }
737
+ if (uncoveredFeatures.length > 0) {
738
+ sections.push(`遗漏功能点 F(${uncoveredFeatures.length} 个,feature-manifest 有、无落地 Scenario):\n${featureList}`);
739
+ }
713
740
  return `
714
741
  🔄 Spec-Code Consistency Fix Loop (${loopCount}/${MAX_LOOP_COUNT})
715
742
 
716
- 发现 ${output.uncoveredScenarios.length} 个未覆盖的 Scenario:
717
-
718
- ${uncoveredList}
743
+ ${sections.join('\n\n')}
719
744
 
720
745
  ⚠️ 铁律:Spec is Truth — 文档与代码冲突时,错的一定是代码。
721
- 🚫 禁止修改 Spec 文件内容(包括 Requirement 名称、Scenario 描述、Given/When/Then 条件)。
746
+ 🚫 绝对禁止为凑覆盖率修改 Spec 文件 / 技术方案 / feature-manifest(包括 Requirement 名称、Scenario 描述、Given/When/Then 条件、[F<n>] 标记)。
722
747
 
723
748
  请通过以下方式修复:
724
- 1. 补充或修改代码实现,使其满足上述 Scenario 描述的业务诉求
725
- 2. 修复后重新运行 spec-code-consistency 检查验证覆盖情况
749
+ 1. missing/partial 项:补齐或修改**代码实现**,使其满足 Scenario 描述的业务诉求(partial 项须命中方法名/接口路径/组件名等核心关键词并通过深度检查)
750
+ 2. 对遗漏功能点 F:补齐对应代码实现(而非补 PRD/技术方案);确因范围裁剪不做的,登记 proposal.md 功能点覆盖对照表并注明理由
751
+ 3. 修复后重新运行 spec-code-consistency 检查验证覆盖情况
726
752
  `;
727
753
  }
728
754
  // ============================================================
@@ -797,6 +823,8 @@ ${legacySection}
797
823
  | Requirements 数量 | ${results.specConsistency.metrics.totalRequirements || 0} |
798
824
  | Scenario 数量 | ${results.specConsistency.metrics.totalScenarios || 0} |
799
825
  | 一致性覆盖率 | ${results.specConsistency.metrics.consistencyRate || 0}% |
826
+ | 疑似未实现 partial 数 | ${results.specConsistency.metrics.partialScenarios?.length || 0} |
827
+ | 遗漏功能点 F | ${results.specConsistency.metrics.uncoveredFeatures?.join(', ') || '—'} |
800
828
  | 循环次数 | ${results.specConsistency.loopCount} |
801
829
  | 状态 | ${results.specConsistency.pass ? '✅ PASS' : '❌ FAIL'} |
802
830
 
@@ -89,8 +89,7 @@ async function runSummarize() {
89
89
  }
90
90
  }
91
91
  // Deviation detection only (no auto review trigger).
92
- // Apply 完成后不再自动触发 review,确保 Stop hook 正确闪断以标记 apply baseline。
93
- // 用户需在新会话中手动执行 `zhuanspec review <change-id>` 进入 review 阶段。
92
+ // Apply 完成后不再自动触发 review;用户准备验收时再手动执行 `zhuanspec review <change-id>`。
94
93
  let deviationDetected = false;
95
94
  if (phase === 'apply' && allTasksComplete) {
96
95
  // Execute post-apply hook to detect deviations only
@@ -71,6 +71,7 @@ export function generateExecutionPlan(tasks, options) {
71
71
  // Create graph and validate
72
72
  const graph = new TaskGraph(incompleteTasks);
73
73
  const validation = graph.validate();
74
+ const taskById = new Map(incompleteTasks.map(task => [task.id, task]));
74
75
  // Check for cycles
75
76
  if (validation.errors.some(e => e.includes('Circular dependency'))) {
76
77
  return {
@@ -95,7 +96,7 @@ export function generateExecutionPlan(tasks, options) {
95
96
  // Initialize
96
97
  for (const task of incompleteTasks) {
97
98
  // Only count dependencies that exist in incomplete tasks
98
- const validDeps = task.depends.filter(d => incompleteTasks.some(t => t.id === d));
99
+ const validDeps = task.depends.filter(d => taskById.has(d));
99
100
  inDegree.set(task.id, validDeps.length);
100
101
  dependents.set(task.id, []);
101
102
  }
@@ -120,7 +121,7 @@ export function generateExecutionPlan(tasks, options) {
120
121
  break;
121
122
  }
122
123
  const readyTasks = readyIds
123
- .map(id => incompleteTasks.find(t => t.id === id))
124
+ .map(id => taskById.get(id))
124
125
  .filter(Boolean);
125
126
  waves.push({
126
127
  wave: waveNumber,