@zhuan-ai/zhuanspec 2.0.0 → 2.1.0
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 +23 -0
- 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 +22 -20
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuan-ai/zhuanspec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zhuanspec",
|
|
@@ -38,6 +38,26 @@
|
|
|
38
38
|
"!dist/**/__tests__",
|
|
39
39
|
"!dist/**/*.map"
|
|
40
40
|
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"lint": "eslint src/",
|
|
43
|
+
"build": "node build.js",
|
|
44
|
+
"dev": "tsc --watch",
|
|
45
|
+
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:ui": "vitest --ui",
|
|
49
|
+
"test:coverage": "vitest --coverage",
|
|
50
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
51
|
+
"prepare": "pnpm run build",
|
|
52
|
+
"prepublishOnly": "pnpm run build",
|
|
53
|
+
"postinstall": "node scripts/postinstall.js",
|
|
54
|
+
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
55
|
+
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
56
|
+
"release": "pnpm run release:ci",
|
|
57
|
+
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
58
|
+
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
59
|
+
"changeset": "changeset"
|
|
60
|
+
},
|
|
41
61
|
"engines": {
|
|
42
62
|
"node": ">=20.19.0"
|
|
43
63
|
},
|
|
@@ -59,23 +79,5 @@
|
|
|
59
79
|
"ora": "^8.2.0",
|
|
60
80
|
"yaml": "^2.8.2",
|
|
61
81
|
"zod": "^4.0.17"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"lint": "eslint src/",
|
|
65
|
-
"build": "node build.js",
|
|
66
|
-
"dev": "tsc --watch",
|
|
67
|
-
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
68
|
-
"test": "vitest run",
|
|
69
|
-
"test:watch": "vitest",
|
|
70
|
-
"test:ui": "vitest --ui",
|
|
71
|
-
"test:coverage": "vitest --coverage",
|
|
72
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
73
|
-
"postinstall": "node scripts/postinstall.js",
|
|
74
|
-
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
75
|
-
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
76
|
-
"release": "pnpm run release:ci",
|
|
77
|
-
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
78
|
-
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
79
|
-
"changeset": "changeset"
|
|
80
82
|
}
|
|
81
|
-
}
|
|
83
|
+
}
|