@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 +9 -4
- package/dist/commands/artifact-workflow.js +10 -35
- package/dist/commands/design.d.ts +3 -2
- package/dist/commands/design.js +27 -19
- package/dist/core/completions/command-registry.js +2 -7
- package/dist/core/configurators/slash/auggie.js +1 -1
- package/dist/core/configurators/slash/costrict.js +1 -1
- package/dist/core/configurators/slash/factory.js +1 -1
- package/dist/core/hooks/init.js +2 -3
- package/dist/core/hooks/post-apply.d.ts +7 -4
- package/dist/core/hooks/post-apply.js +74 -5
- package/dist/core/hooks/review-hooks.d.ts +19 -0
- package/dist/core/hooks/review-hooks.js +38 -10
- package/dist/core/hooks/summarize.js +1 -2
- package/dist/core/task-graph/execution-planner.js +3 -2
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +7 -9
- package/dist/core/templates/skill-templates.js +14 -50
- package/dist/core/templates/slash-command-templates.js +97 -24
- package/dist/core/templates/tasks-template.js +3 -3
- package/dist/core/templates/tdd-tasks-template.js +4 -4
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +4 -2
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('--
|
|
108
|
-
.
|
|
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,
|
|
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,
|
|
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.
|
|
458
|
+
instruction = `All tasks are complete. Apply is finished.
|
|
479
459
|
|
|
480
460
|
**Required Next Steps**
|
|
481
461
|
|
|
482
|
-
1.
|
|
483
|
-
2.
|
|
484
|
-
3.
|
|
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
|
|
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('###
|
|
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.
|
|
596
|
-
console.log('2.
|
|
597
|
-
console.log('3.
|
|
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
|
-
|
|
8
|
-
|
|
7
|
+
feishuUrl?: string;
|
|
8
|
+
legacyPageId?: string;
|
|
9
|
+
legacyUrl?: string;
|
|
9
10
|
output?: string;
|
|
10
11
|
validate?: boolean;
|
|
11
12
|
autoFix?: boolean;
|
package/dist/commands/design.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
80
|
-
|
|
80
|
+
feishuUrl: options?.feishuUrl,
|
|
81
|
+
legacyPageId: options?.legacyPageId,
|
|
82
|
+
legacyUrl: options?.legacyUrl,
|
|
81
83
|
});
|
|
82
|
-
if (!designContext.desc && !designContext.
|
|
83
|
-
throw new Error('Missing design source. Provide --desc
|
|
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
|
-
|
|
116
|
-
|
|
117
|
+
feishuUrl: input.feishuUrl?.trim() ?? '',
|
|
118
|
+
legacyPageId: input.legacyPageId?.trim() ?? '',
|
|
119
|
+
legacyUrl: input.legacyUrl?.trim() ?? '',
|
|
117
120
|
};
|
|
118
|
-
if (result.desc || result.
|
|
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: '
|
|
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
|
-
`-
|
|
151
|
-
|
|
153
|
+
`- 飞书文档 URL: ${context.feishuUrl || context.legacyUrl || '(未提供)'}`,
|
|
154
|
+
'- 飞书读取方式: 使用飞书 CLI `lark-cli` 读取正文',
|
|
152
155
|
`- 绑定变更: ${boundChange || '(无,独立于提案阶段)'}`,
|
|
153
156
|
'',
|
|
154
157
|
'## 生成要求',
|
|
155
|
-
'-
|
|
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.
|
|
350
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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: '
|
|
90
|
-
description: '
|
|
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:
|
|
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:
|
|
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:
|
|
17
|
+
argument-hint: feishu-url or description
|
|
18
18
|
---`,
|
|
19
19
|
apply: `---
|
|
20
20
|
description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
|
package/dist/core/hooks/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
25
|
-
*
|
|
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
|
|
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
|
|
43
|
-
*
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
${uncoveredList}
|
|
743
|
+
${sections.join('\n\n')}
|
|
719
744
|
|
|
720
745
|
⚠️ 铁律:Spec is Truth — 文档与代码冲突时,错的一定是代码。
|
|
721
|
-
🚫
|
|
746
|
+
🚫 绝对禁止为凑覆盖率修改 Spec 文件 / 技术方案 / feature-manifest(包括 Requirement 名称、Scenario 描述、Given/When/Then 条件、[F<n>] 标记)。
|
|
722
747
|
|
|
723
748
|
请通过以下方式修复:
|
|
724
|
-
1.
|
|
725
|
-
2.
|
|
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
|
|
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 =>
|
|
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 =>
|
|
124
|
+
.map(id => taskById.get(id))
|
|
124
125
|
.filter(Boolean);
|
|
125
126
|
waves.push({
|
|
126
127
|
wave: waveNumber,
|