@zhuan-ai/zhuanspec 2.0.0 → 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/README.zh.md +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/commands/artifact-workflow.js +22 -2
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +65 -3
- package/dist/core/skill-discovery.d.ts +2 -2
- package/dist/core/skill-discovery.js +16 -3
- package/dist/core/task-graph/index.d.ts +2 -0
- package/dist/core/task-graph/index.js +2 -0
- package/dist/core/task-graph/mermaid-renderer.d.ts +22 -0
- package/dist/core/task-graph/mermaid-renderer.js +128 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +60 -37
- package/dist/core/templates/skill-templates.js +42 -0
- package/dist/core/templates/slash-command-templates.js +25 -2
- package/dist/core/templates/tasks-template.d.ts +23 -0
- package/dist/core/templates/tasks-template.js +79 -0
- package/dist/core/templates/tdd-tasks-template.d.ts +24 -0
- package/dist/core/templates/tdd-tasks-template.js +116 -0
- package/dist/core/validation/strict-rules.d.ts +60 -0
- package/dist/core/validation/strict-rules.js +287 -0
- package/dist/core/validation/types.d.ts +10 -0
- package/dist/core/validation/validator.d.ts +5 -0
- package/dist/core/validation/validator.js +103 -1
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +13 -13
- package/schemas/spec-driven/templates/design.md +6 -6
- package/schemas/spec-driven/templates/proposal.md +7 -7
- package/schemas/spec-driven/templates/spec.md +4 -4
- package/schemas/tdd/schema.yaml +107 -107
- package/schemas/tdd/templates/implementation.md +5 -5
- package/schemas/tdd/templates/spec.md +6 -6
- package/schemas/tdd/templates/test.md +7 -7
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict Validation Rules
|
|
3
|
+
*
|
|
4
|
+
* Implementation of three strict validation rules for `zhuanspec validate --strict`:
|
|
5
|
+
* 1. pre-clarification-completed: Check Pre-Clarification Log section
|
|
6
|
+
* 2. skill-tags-valid: Validate @skill tags against known skills
|
|
7
|
+
* 3. task-ordering-by-wave: Verify task ordering follows wave structure
|
|
8
|
+
*/
|
|
9
|
+
import { generateExecutionPlan } from '../task-graph/execution-planner.js';
|
|
10
|
+
// Patterns for detecting unresolved markers
|
|
11
|
+
const UNRESOLVED_MARKERS = /\b(TODO|TBD|PENDING|\?)\b/i;
|
|
12
|
+
const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
|
|
13
|
+
const PRE_CLARIFICATION_SECTION_PATTERN = /^##\s+Pre-Clarification\s+Log\s*$/im;
|
|
14
|
+
const VALID_COMPLETION_MARKERS = [
|
|
15
|
+
/no\s+clarification\s+needed/i,
|
|
16
|
+
/status:\s*completed/i,
|
|
17
|
+
];
|
|
18
|
+
// Skill mapping section header pattern
|
|
19
|
+
const SKILL_MAPPING_SECTION_PATTERN = /^##\s+Skill\s+Mapping\s*$/im;
|
|
20
|
+
// Table data row pattern (starts with |, not a separator row like |---|---|)
|
|
21
|
+
const TABLE_DATA_ROW_PATTERN = /^\|(?![-:|\s]+\|$)[^\n]+\|/gm;
|
|
22
|
+
const SKILL_TAG_PATTERN = /@skill:([^\s@]+)/g;
|
|
23
|
+
// Wave header pattern
|
|
24
|
+
const WAVE_HEADER_PATTERN = /^###\s+Wave\s+(\d+)/im;
|
|
25
|
+
/**
|
|
26
|
+
* Rule 1: pre-clarification-completed
|
|
27
|
+
*
|
|
28
|
+
* Checks that tasks.md has a valid "## Pre-Clarification Log" section.
|
|
29
|
+
* Valid content includes:
|
|
30
|
+
* - Actual Q&A record text
|
|
31
|
+
* - "No clarification needed"
|
|
32
|
+
* - "Status: COMPLETED"
|
|
33
|
+
* Invalid:
|
|
34
|
+
* - Section missing
|
|
35
|
+
* - Section empty (whitespace only)
|
|
36
|
+
* - Only HTML comments
|
|
37
|
+
* - Contains "TODO", "TBD", "PENDING", "?" unresolved markers
|
|
38
|
+
*/
|
|
39
|
+
export function checkPreClarification(tasksContent) {
|
|
40
|
+
const result = {
|
|
41
|
+
ruleId: 'pre-clarification-completed',
|
|
42
|
+
ruleName: 'Pre-clarification completed',
|
|
43
|
+
passed: false,
|
|
44
|
+
errors: [],
|
|
45
|
+
warnings: [],
|
|
46
|
+
};
|
|
47
|
+
// Check if section exists
|
|
48
|
+
const sectionMatch = tasksContent.match(PRE_CLARIFICATION_SECTION_PATTERN);
|
|
49
|
+
if (!sectionMatch) {
|
|
50
|
+
result.errors.push('Pre-Clarification Log section is missing');
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// Extract section content (from header to next ## or end)
|
|
54
|
+
const sectionStartIndex = sectionMatch.index + sectionMatch[0].length;
|
|
55
|
+
const nextSectionMatch = tasksContent.slice(sectionStartIndex).match(/^##\s+/m);
|
|
56
|
+
const sectionEndIndex = nextSectionMatch
|
|
57
|
+
? sectionStartIndex + nextSectionMatch.index
|
|
58
|
+
: tasksContent.length;
|
|
59
|
+
let sectionContent = tasksContent.slice(sectionStartIndex, sectionEndIndex);
|
|
60
|
+
// Remove HTML comments for content analysis
|
|
61
|
+
const contentWithoutComments = sectionContent.replace(HTML_COMMENT_PATTERN, '').trim();
|
|
62
|
+
// Check if section is empty
|
|
63
|
+
if (!contentWithoutComments) {
|
|
64
|
+
result.errors.push('Pre-Clarification Log section is empty or contains only HTML comments');
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// Check for unresolved markers
|
|
68
|
+
if (UNRESOLVED_MARKERS.test(contentWithoutComments)) {
|
|
69
|
+
result.errors.push('Pre-Clarification Log contains unresolved markers (TODO, TBD, PENDING, or ?)');
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
// Check for valid completion markers or actual content
|
|
73
|
+
const hasValidCompletionMarker = VALID_COMPLETION_MARKERS.some(pattern => pattern.test(contentWithoutComments));
|
|
74
|
+
const hasSubstantiveContent = contentWithoutComments.length > 20; // More than just a few words
|
|
75
|
+
if (hasValidCompletionMarker || hasSubstantiveContent) {
|
|
76
|
+
result.passed = true;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
result.errors.push('Pre-Clarification Log does not contain valid completion markers or substantive Q&A content');
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Rule 2: skill-tags-valid
|
|
85
|
+
*
|
|
86
|
+
* Validates:
|
|
87
|
+
* a. Skill Mapping table exists with at least one data row
|
|
88
|
+
* b. Each task has @skill:xxx tag or @skill:none
|
|
89
|
+
* c. @skill:none tasks should have a reason in description
|
|
90
|
+
* d. All @skill:xxx names must exist in knownSkillNames
|
|
91
|
+
* e. Comma-separated multi-skills are validated individually
|
|
92
|
+
*/
|
|
93
|
+
export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames) {
|
|
94
|
+
const result = {
|
|
95
|
+
ruleId: 'skill-tags-valid',
|
|
96
|
+
ruleName: 'Skill tags valid',
|
|
97
|
+
passed: false,
|
|
98
|
+
errors: [],
|
|
99
|
+
warnings: [],
|
|
100
|
+
};
|
|
101
|
+
// a. Check Skill Mapping section exists with table data
|
|
102
|
+
const sectionMatch = tasksContent.match(SKILL_MAPPING_SECTION_PATTERN);
|
|
103
|
+
if (!sectionMatch) {
|
|
104
|
+
result.warnings.push('Skill Mapping section not found');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Extract section content (from header to next ## or end)
|
|
108
|
+
const sectionStartIndex = sectionMatch.index + sectionMatch[0].length;
|
|
109
|
+
const nextSectionMatch = tasksContent.slice(sectionStartIndex).match(/^##\s+/m);
|
|
110
|
+
const sectionEndIndex = nextSectionMatch
|
|
111
|
+
? sectionStartIndex + nextSectionMatch.index
|
|
112
|
+
: tasksContent.length;
|
|
113
|
+
const sectionContent = tasksContent.slice(sectionStartIndex, sectionEndIndex);
|
|
114
|
+
// Check for table data rows (lines starting with | that are not separator rows)
|
|
115
|
+
const tableRows = sectionContent.match(TABLE_DATA_ROW_PATTERN);
|
|
116
|
+
// Filter out header rows and separator rows
|
|
117
|
+
const dataRows = tableRows?.filter(row => {
|
|
118
|
+
const trimmed = row.trim();
|
|
119
|
+
// Skip separator rows like |---|---| or |:---|---:|
|
|
120
|
+
if (/^\|[-:\s|]+\|$/.test(trimmed))
|
|
121
|
+
return false;
|
|
122
|
+
// Skip header rows (first non-separator row is header)
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
// Need at least 2 rows (header + 1 data row)
|
|
126
|
+
if (!dataRows || dataRows.length < 2) {
|
|
127
|
+
result.warnings.push('Skill Mapping table has no data rows');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// If no tasks, consider it valid
|
|
131
|
+
if (parsedTasks.length === 0) {
|
|
132
|
+
result.passed = true;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
// b, c, d, e. Validate each task's skill tags
|
|
136
|
+
const knownSkillSet = new Set(knownSkillNames.map(s => s.toLowerCase()));
|
|
137
|
+
for (const task of parsedTasks) {
|
|
138
|
+
// b. Check if task has skill tag
|
|
139
|
+
if (task.skills.length === 0) {
|
|
140
|
+
result.errors.push(`Task ${task.id}: missing @skill tag`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Validate each skill
|
|
144
|
+
for (const skill of task.skills) {
|
|
145
|
+
const skillLower = skill.toLowerCase();
|
|
146
|
+
// c. @skill:none should have a reason
|
|
147
|
+
if (skillLower === 'none') {
|
|
148
|
+
// Check if description mentions why no skill needed
|
|
149
|
+
const descLower = task.description.toLowerCase();
|
|
150
|
+
const hasReason = descLower.includes('manual') ||
|
|
151
|
+
descLower.includes('review') ||
|
|
152
|
+
descLower.includes('document') ||
|
|
153
|
+
descLower.includes('config') ||
|
|
154
|
+
descLower.includes('test') ||
|
|
155
|
+
descLower.includes('verify') ||
|
|
156
|
+
task.description.length > 30; // Substantial description is acceptable
|
|
157
|
+
if (!hasReason) {
|
|
158
|
+
result.warnings.push(`Task ${task.id}: @skill:none should have a reason in description`);
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// d. Validate skill exists in known skills
|
|
163
|
+
if (knownSkillNames.length > 0 && !knownSkillSet.has(skillLower)) {
|
|
164
|
+
const availableSkills = knownSkillNames.length <= 10
|
|
165
|
+
? knownSkillNames.join(', ')
|
|
166
|
+
: `${knownSkillNames.slice(0, 10).join(', ')}... (${knownSkillNames.length} total)`;
|
|
167
|
+
result.errors.push(`Task ${task.id}: @skill:${skill} is not a known skill. Available skills: ${availableSkills}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Passed if no errors
|
|
172
|
+
result.passed = result.errors.length === 0;
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Rule 3: task-ordering-by-wave
|
|
177
|
+
*
|
|
178
|
+
* Validates:
|
|
179
|
+
* - Tasks are ordered by wave (wave 1 tasks before wave 2, etc.)
|
|
180
|
+
* - Wave headers "### Wave N" match actual computed wave numbers
|
|
181
|
+
* - Tasks.md must have Wave headers
|
|
182
|
+
*/
|
|
183
|
+
export function checkTaskOrderingByWave(tasksContent, parsedTasks) {
|
|
184
|
+
const result = {
|
|
185
|
+
ruleId: 'task-ordering-by-wave',
|
|
186
|
+
ruleName: 'Task ordering by wave',
|
|
187
|
+
passed: false,
|
|
188
|
+
errors: [],
|
|
189
|
+
warnings: [],
|
|
190
|
+
};
|
|
191
|
+
// If no tasks, consider it valid
|
|
192
|
+
if (parsedTasks.length === 0) {
|
|
193
|
+
result.passed = true;
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
// Check for wave headers
|
|
197
|
+
const waveHeaders = [];
|
|
198
|
+
const waveHeaderRegex = /^###\s+Wave\s+(\d+)/gim;
|
|
199
|
+
let match;
|
|
200
|
+
while ((match = waveHeaderRegex.exec(tasksContent)) !== null) {
|
|
201
|
+
waveHeaders.push({
|
|
202
|
+
wave: parseInt(match[1], 10),
|
|
203
|
+
position: match.index,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (waveHeaders.length === 0) {
|
|
207
|
+
result.errors.push('No Wave headers found in tasks.md. Expected format: "### Wave N"');
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
// Generate execution plan to get actual waves
|
|
211
|
+
const executionPlan = generateExecutionPlan(parsedTasks);
|
|
212
|
+
if (executionPlan.hasCycle) {
|
|
213
|
+
result.errors.push('Task dependency graph has cycles, cannot validate wave ordering');
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
// Build a map of task ID to actual wave number
|
|
217
|
+
const taskToWave = new Map();
|
|
218
|
+
for (const wave of executionPlan.waves) {
|
|
219
|
+
for (const task of wave.tasks) {
|
|
220
|
+
taskToWave.set(task.id, wave.wave);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Find task positions in content
|
|
224
|
+
const taskPositions = [];
|
|
225
|
+
for (const task of parsedTasks) {
|
|
226
|
+
// Find task line in content
|
|
227
|
+
const taskPattern = new RegExp(`-\\s+\\[[ xX]\\]\\s+${task.id.replace('.', '\\.')}\\s`, 'gm');
|
|
228
|
+
const taskMatch = taskPattern.exec(tasksContent);
|
|
229
|
+
if (taskMatch) {
|
|
230
|
+
const actualWave = taskToWave.get(task.id);
|
|
231
|
+
if (actualWave !== undefined) {
|
|
232
|
+
taskPositions.push({
|
|
233
|
+
id: task.id,
|
|
234
|
+
position: taskMatch.index,
|
|
235
|
+
actualWave,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Sort by position in file
|
|
241
|
+
taskPositions.sort((a, b) => a.position - b.position);
|
|
242
|
+
// Check ordering: all tasks of wave N should appear before tasks of wave N+1
|
|
243
|
+
let lastWave = 0;
|
|
244
|
+
for (const task of taskPositions) {
|
|
245
|
+
if (task.actualWave < lastWave) {
|
|
246
|
+
result.errors.push(`Task ${task.id} (wave ${task.actualWave}) appears after tasks of wave ${lastWave}`);
|
|
247
|
+
}
|
|
248
|
+
lastWave = Math.max(lastWave, task.actualWave);
|
|
249
|
+
}
|
|
250
|
+
// Validate wave headers match actual wave numbers
|
|
251
|
+
const actualWaveNumbers = new Set(executionPlan.waves.map(w => w.wave));
|
|
252
|
+
const declaredWaveNumbers = new Set(waveHeaders.map(h => h.wave));
|
|
253
|
+
for (const declared of declaredWaveNumbers) {
|
|
254
|
+
if (!actualWaveNumbers.has(declared)) {
|
|
255
|
+
result.warnings.push(`Wave header "### Wave ${declared}" does not match any computed wave`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const actual of actualWaveNumbers) {
|
|
259
|
+
if (!declaredWaveNumbers.has(actual)) {
|
|
260
|
+
result.errors.push(`Missing Wave header for computed wave ${actual}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Check wave header ordering
|
|
264
|
+
for (let i = 1; i < waveHeaders.length; i++) {
|
|
265
|
+
if (waveHeaders[i].wave <= waveHeaders[i - 1].wave) {
|
|
266
|
+
result.errors.push(`Wave headers are not in ascending order: Wave ${waveHeaders[i - 1].wave} followed by Wave ${waveHeaders[i].wave}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
result.passed = result.errors.length === 0;
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Main entry: Run all strict validation rules
|
|
274
|
+
*/
|
|
275
|
+
export async function runStrictValidation(tasksContent, parsedTasks, knownSkillNames) {
|
|
276
|
+
const checks = [
|
|
277
|
+
checkPreClarification(tasksContent),
|
|
278
|
+
checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames),
|
|
279
|
+
checkTaskOrderingByWave(tasksContent, parsedTasks),
|
|
280
|
+
];
|
|
281
|
+
const allPassed = checks.every(check => check.passed);
|
|
282
|
+
return {
|
|
283
|
+
checks,
|
|
284
|
+
allPassed,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=strict-rules.js.map
|
|
@@ -6,6 +6,13 @@ export interface ValidationIssue {
|
|
|
6
6
|
line?: number;
|
|
7
7
|
column?: number;
|
|
8
8
|
}
|
|
9
|
+
export interface StrictCheckSummary {
|
|
10
|
+
ruleId: string;
|
|
11
|
+
ruleName: string;
|
|
12
|
+
passed: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
warnings: string[];
|
|
15
|
+
}
|
|
9
16
|
export interface ValidationReport {
|
|
10
17
|
valid: boolean;
|
|
11
18
|
issues: ValidationIssue[];
|
|
@@ -14,5 +21,8 @@ export interface ValidationReport {
|
|
|
14
21
|
warnings: number;
|
|
15
22
|
info: number;
|
|
16
23
|
};
|
|
24
|
+
strictChecks?: StrictCheckSummary[];
|
|
25
|
+
/** Mermaid diagram content if task graph analysis succeeded */
|
|
26
|
+
mermaidDiagram?: string;
|
|
17
27
|
}
|
|
18
28
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -29,5 +29,10 @@ export declare class Validator {
|
|
|
29
29
|
private containsShallOrMust;
|
|
30
30
|
private countScenarios;
|
|
31
31
|
private formatSectionList;
|
|
32
|
+
/**
|
|
33
|
+
* Find project root directory by walking up from changeDir.
|
|
34
|
+
* Looks for common project markers like package.json, .git, or skills directory.
|
|
35
|
+
*/
|
|
36
|
+
private findProjectRoot;
|
|
32
37
|
}
|
|
33
38
|
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -6,6 +6,12 @@ import { ChangeParser } from '../parsers/change-parser.js';
|
|
|
6
6
|
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
7
7
|
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
|
|
8
8
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
9
|
+
import { runStrictValidation } from './strict-rules.js';
|
|
10
|
+
import { discoverSkills } from '../skill-discovery.js';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { parseTasks } from '../task-graph/task-parser.js';
|
|
13
|
+
import { generateExecutionPlan } from '../task-graph/execution-planner.js';
|
|
14
|
+
import { renderMermaidDiagram } from '../task-graph/mermaid-renderer.js';
|
|
9
15
|
export class Validator {
|
|
10
16
|
strictMode;
|
|
11
17
|
constructor(strictMode = false) {
|
|
@@ -253,7 +259,84 @@ export class Validator {
|
|
|
253
259
|
if (totalDeltas === 0) {
|
|
254
260
|
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
|
255
261
|
}
|
|
256
|
-
|
|
262
|
+
// Strict mode validation for tasks.md
|
|
263
|
+
let strictChecks;
|
|
264
|
+
if (this.strictMode) {
|
|
265
|
+
const tasksFile = path.join(changeDir, 'tasks.md');
|
|
266
|
+
let tasksContent;
|
|
267
|
+
try {
|
|
268
|
+
tasksContent = await fs.readFile(tasksFile, 'utf-8');
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// tasks.md not found - skip strict validation for tasks
|
|
272
|
+
}
|
|
273
|
+
if (tasksContent) {
|
|
274
|
+
const parsedTasks = parseTasks(tasksContent);
|
|
275
|
+
// Get known skill names
|
|
276
|
+
let knownSkillNames = [];
|
|
277
|
+
try {
|
|
278
|
+
const projectRoot = this.findProjectRoot(changeDir);
|
|
279
|
+
const skills = discoverSkills(projectRoot);
|
|
280
|
+
knownSkillNames = skills.map(s => s.name);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// skill discovery failed - add warning and continue without skill name validation
|
|
284
|
+
issues.push({
|
|
285
|
+
level: 'WARNING',
|
|
286
|
+
path: 'tasks.md',
|
|
287
|
+
message: 'Could not discover skills for strict validation — skill name check skipped',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// Run strict validation
|
|
291
|
+
const strictResult = await runStrictValidation(tasksContent, parsedTasks, knownSkillNames);
|
|
292
|
+
strictChecks = strictResult.checks;
|
|
293
|
+
// Merge strict errors into main issues
|
|
294
|
+
for (const check of strictResult.checks) {
|
|
295
|
+
for (const error of check.errors) {
|
|
296
|
+
issues.push({
|
|
297
|
+
level: 'ERROR',
|
|
298
|
+
path: 'tasks.md',
|
|
299
|
+
message: `[${check.ruleName}] ${error}`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
for (const warning of check.warnings) {
|
|
303
|
+
issues.push({
|
|
304
|
+
level: 'WARNING',
|
|
305
|
+
path: 'tasks.md',
|
|
306
|
+
message: `[${check.ruleName}] ${warning}`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Generate Mermaid diagram if tasks.md exists and has tasks
|
|
313
|
+
let mermaidDiagram;
|
|
314
|
+
const tasksFile = path.join(changeDir, 'tasks.md');
|
|
315
|
+
let tasksContentForMermaid;
|
|
316
|
+
try {
|
|
317
|
+
tasksContentForMermaid = await fs.readFile(tasksFile, 'utf-8');
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// tasks.md not found - skip Mermaid generation
|
|
321
|
+
}
|
|
322
|
+
if (tasksContentForMermaid) {
|
|
323
|
+
const parsedTasksForMermaid = parseTasks(tasksContentForMermaid);
|
|
324
|
+
if (parsedTasksForMermaid.length > 0) {
|
|
325
|
+
const executionPlan = generateExecutionPlan(parsedTasksForMermaid);
|
|
326
|
+
// Only generate Mermaid diagram if execution plan is valid (no cycles)
|
|
327
|
+
if (!executionPlan.hasCycle && executionPlan.waves.length > 0) {
|
|
328
|
+
mermaidDiagram = renderMermaidDiagram(executionPlan, parsedTasksForMermaid);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const report = this.createReport(issues);
|
|
333
|
+
if (strictChecks) {
|
|
334
|
+
report.strictChecks = strictChecks;
|
|
335
|
+
}
|
|
336
|
+
if (mermaidDiagram) {
|
|
337
|
+
report.mermaidDiagram = mermaidDiagram;
|
|
338
|
+
}
|
|
339
|
+
return report;
|
|
257
340
|
}
|
|
258
341
|
convertZodErrors(error) {
|
|
259
342
|
return error.issues.map(err => {
|
|
@@ -405,5 +488,24 @@ export class Validator {
|
|
|
405
488
|
const last = sections[sections.length - 1];
|
|
406
489
|
return `${head.join(', ')} and ${last}`;
|
|
407
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Find project root directory by walking up from changeDir.
|
|
493
|
+
* Looks for common project markers like package.json, .git, or skills directory.
|
|
494
|
+
*/
|
|
495
|
+
findProjectRoot(changeDir) {
|
|
496
|
+
let current = path.resolve(changeDir);
|
|
497
|
+
const root = path.parse(current).root;
|
|
498
|
+
while (current !== root) {
|
|
499
|
+
// Check for project root markers
|
|
500
|
+
if (existsSync(path.join(current, 'package.json')) ||
|
|
501
|
+
existsSync(path.join(current, '.git')) ||
|
|
502
|
+
existsSync(path.join(current, 'skills'))) {
|
|
503
|
+
return current;
|
|
504
|
+
}
|
|
505
|
+
current = path.dirname(current);
|
|
506
|
+
}
|
|
507
|
+
// Fallback to process.cwd() if no markers found
|
|
508
|
+
return process.cwd();
|
|
509
|
+
}
|
|
408
510
|
}
|
|
409
511
|
//# sourceMappingURL=validator.js.map
|
package/package.json
CHANGED
|
@@ -44,19 +44,19 @@ artifacts:
|
|
|
44
44
|
创建建立此变更原因的提案文档。
|
|
45
45
|
|
|
46
46
|
章节:
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- **Skill
|
|
47
|
+
- **变更原因**:关于问题或机会的 1-2 句话。它解决了什么问题?为什么是现在?
|
|
48
|
+
- **变更内容**:变更的要点列表。具体说明新功能、修改或移除。用 **BREAKING** 标记破坏性更改。
|
|
49
|
+
- **功能范围**:确定将创建或修改哪些规范:
|
|
50
|
+
- **新增功能**:列出正在引入的功能。每个都会成为新的 `specs/<name>/spec.md`。使用 kebab-case 名称(例如,`user-auth`、`data-export`)。
|
|
51
|
+
- **修改功能**:列出其 REQUIREMENTS 正在更改的现有功能。仅当规范级行为更改时(不仅仅是实现细节)才包含。每个都需要一个增量规范文件。检查 `openspec/specs/` 以获取现有规范名称。如果没有要求更改,则留空。
|
|
52
|
+
- **Skill 映射**:运行 `zhuanspec skills list` 发现可用 skill,基于 skill description 进行语义匹配:
|
|
53
53
|
1. 仔细阅读每个 skill 的 description,理解其具体功能和适用场景
|
|
54
54
|
2. 只有当实现区域的实际功能与 skill description 明确匹配时才关联
|
|
55
55
|
3. 简单的代码修改(如枚举值增删)应匹配通用编码规范 skill,而非架构级 skill
|
|
56
56
|
4. 避免仅因模块名称或文件路径中的关键词而错误匹配
|
|
57
57
|
5. 在表格中说明匹配理由,确保映射合理性
|
|
58
58
|
此映射将在 tasks.md 中用于 @skill 标注。如果没有匹配 skill 则留空。
|
|
59
|
-
-
|
|
59
|
+
- **影响范围**:受影响的代码、API、依赖项或系统。
|
|
60
60
|
|
|
61
61
|
重要提示:Capabilities 部分至关重要。它在提案和规范阶段之间创建契约。在填写之前,请研究现有规范。
|
|
62
62
|
此处列出的每个功能都需要一个相应的规范文件。
|
|
@@ -134,12 +134,12 @@ artifacts:
|
|
|
134
134
|
- 在编码之前从技术决策中受益的模糊性
|
|
135
135
|
|
|
136
136
|
章节:
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
137
|
+
- **背景**:背景、当前状态、约束、利益相关者
|
|
138
|
+
- **目标 / 非目标**:此设计实现的内容和明确排除的内容
|
|
139
|
+
- **决策**:关键的技术选择及其理由(为什么选择 X 而不是 Y?)。包括为每个决策考虑的替代方案。
|
|
140
|
+
- **风险 / 权衡**:已知的限制,可能出错的事情。格式:[风险] → 缓解措施
|
|
141
|
+
- **迁移计划**:部署步骤、回滚策略(如果适用)
|
|
142
|
+
- **待解决问题**:待解决的决定或未知数
|
|
143
143
|
|
|
144
144
|
专注于架构和方法,而不是逐行实现。
|
|
145
145
|
参考提案了解动机,参考规范了解要求。
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
##
|
|
1
|
+
## 背景
|
|
2
2
|
|
|
3
3
|
<!-- 背景和当前状态 -->
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 目标 / 非目标
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**目标:**
|
|
8
8
|
<!-- 此设计旨在实现的目标 -->
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**非目标:**
|
|
11
11
|
<!-- 明确不在范围内的内容 -->
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 决策
|
|
14
14
|
|
|
15
15
|
<!-- 关键设计决策和理由 -->
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## 风险 / 权衡
|
|
18
18
|
|
|
19
19
|
<!-- 已知风险和权衡 -->
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
##
|
|
1
|
+
## 变更原因
|
|
2
2
|
|
|
3
3
|
<!-- 解释此变更的动机。它解决了什么问题?为什么是现在? -->
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 变更内容
|
|
6
6
|
|
|
7
7
|
<!-- 描述将要更改的内容。具体说明新功能、修改或移除。 -->
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 功能范围
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### 新增功能
|
|
12
12
|
<!-- 正在引入的功能。将 <name> 替换为 kebab-case 标识符(例如,user-auth、data-export、api-rate-limiting)。每个都会创建 specs/<name>/spec.md -->
|
|
13
13
|
- `<name>`: <此功能涵盖内容的简要描述>
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### 修改功能
|
|
16
16
|
<!-- 其 REQUIREMENTS 正在更改的现有功能(不仅仅是实现)。
|
|
17
17
|
仅当规范级行为更改时,才在此处列出。每个都需要一个增量规范文件。
|
|
18
18
|
使用 zhuanspec/specs/ 中的现有规范名称。如果没有要求更改,则留空。 -->
|
|
19
19
|
- `<existing-name>`: <正在更改的要求>
|
|
20
20
|
|
|
21
|
-
## Skill
|
|
21
|
+
## Skill 映射
|
|
22
22
|
|
|
23
23
|
<!-- 重要:运行 `zhuanspec skills list` 查看可用 skill,基于 skill description 进行语义匹配。
|
|
24
24
|
匹配原则:
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
|---------|---------|-----------|---------|
|
|
39
39
|
| <!-- 模块/文件 --> | <!-- 具体功能 --> | <!-- skill 名称 --> | <!-- 为什么匹配 --> |
|
|
40
40
|
|
|
41
|
-
##
|
|
41
|
+
## 影响范围
|
|
42
42
|
|
|
43
43
|
<!-- 受影响的代码、API、依赖项、系统 -->
|