@zhuan-ai/zhuanspec 2.2.4 → 2.4.8

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.
Files changed (103) hide show
  1. package/bin/zhuanspec-hook.js +3 -0
  2. package/dist/cli/hooks.d.ts +14 -0
  3. package/dist/cli/hooks.js +465 -0
  4. package/dist/cli/index.js +100 -0
  5. package/dist/commands/artifact-workflow.js +15 -34
  6. package/dist/commands/design.d.ts +42 -0
  7. package/dist/commands/design.js +337 -0
  8. package/dist/commands/progress.d.ts +32 -0
  9. package/dist/commands/progress.js +278 -0
  10. package/dist/commands/review.d.ts +32 -0
  11. package/dist/commands/review.js +472 -0
  12. package/dist/commands/validate.d.ts +14 -0
  13. package/dist/commands/validate.js +161 -10
  14. package/dist/core/archive.d.ts +1 -0
  15. package/dist/core/archive.js +45 -3
  16. package/dist/core/completions/command-registry.js +67 -0
  17. package/dist/core/configurators/slash/amazon-q.js +32 -2
  18. package/dist/core/configurators/slash/antigravity.js +8 -2
  19. package/dist/core/configurators/slash/auggie.js +16 -1
  20. package/dist/core/configurators/slash/base.js +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +4 -0
  22. package/dist/core/configurators/slash/claude.js +56 -1
  23. package/dist/core/configurators/slash/cline.js +8 -2
  24. package/dist/core/configurators/slash/codebuddy.js +22 -1
  25. package/dist/core/configurators/slash/codex.js +21 -0
  26. package/dist/core/configurators/slash/costrict.js +15 -0
  27. package/dist/core/configurators/slash/crush.js +22 -1
  28. package/dist/core/configurators/slash/cursor.js +22 -1
  29. package/dist/core/configurators/slash/factory.js +16 -1
  30. package/dist/core/configurators/slash/gemini.js +8 -2
  31. package/dist/core/configurators/slash/github-copilot.js +19 -1
  32. package/dist/core/configurators/slash/iflow.js +22 -1
  33. package/dist/core/configurators/slash/kilocode.js +4 -1
  34. package/dist/core/configurators/slash/opencode.js +27 -0
  35. package/dist/core/configurators/slash/qoder.d.ts +4 -0
  36. package/dist/core/configurators/slash/qoder.js +59 -1
  37. package/dist/core/configurators/slash/qwen.js +8 -2
  38. package/dist/core/configurators/slash/roocode.js +8 -2
  39. package/dist/core/configurators/slash/windsurf.js +8 -2
  40. package/dist/core/dashboard/metrics.d.ts +33 -0
  41. package/dist/core/dashboard/metrics.js +114 -0
  42. package/dist/core/hooks/collect-knowledge.d.ts +16 -0
  43. package/dist/core/hooks/collect-knowledge.js +203 -0
  44. package/dist/core/hooks/context-load-hook.d.ts +25 -0
  45. package/dist/core/hooks/context-load-hook.js +159 -0
  46. package/dist/core/hooks/deviation-check.d.ts +27 -0
  47. package/dist/core/hooks/deviation-check.js +403 -0
  48. package/dist/core/hooks/deviation-handler.d.ts +43 -0
  49. package/dist/core/hooks/deviation-handler.js +98 -0
  50. package/dist/core/hooks/init.d.ts +14 -0
  51. package/dist/core/hooks/init.js +244 -0
  52. package/dist/core/hooks/notify-milestone.d.ts +14 -0
  53. package/dist/core/hooks/notify-milestone.js +170 -0
  54. package/dist/core/hooks/post-apply.d.ts +29 -0
  55. package/dist/core/hooks/post-apply.js +173 -0
  56. package/dist/core/hooks/post-archive.d.ts +7 -0
  57. package/dist/core/hooks/post-archive.js +208 -0
  58. package/dist/core/hooks/pre-apply.d.ts +34 -0
  59. package/dist/core/hooks/pre-apply.js +139 -0
  60. package/dist/core/hooks/pre-archive.d.ts +7 -0
  61. package/dist/core/hooks/pre-archive.js +50 -0
  62. package/dist/core/hooks/record-progress.d.ts +49 -0
  63. package/dist/core/hooks/record-progress.js +494 -0
  64. package/dist/core/hooks/review-hooks.d.ts +89 -0
  65. package/dist/core/hooks/review-hooks.js +345 -0
  66. package/dist/core/hooks/review-orchestrator.d.ts +40 -0
  67. package/dist/core/hooks/review-orchestrator.js +146 -0
  68. package/dist/core/hooks/summarize.d.ts +15 -0
  69. package/dist/core/hooks/summarize.js +282 -0
  70. package/dist/core/hooks/user-input-hook.d.ts +25 -0
  71. package/dist/core/hooks/user-input-hook.js +179 -0
  72. package/dist/core/init.d.ts +8 -0
  73. package/dist/core/init.js +251 -23
  74. package/dist/core/parsers/requirement-blocks.js +13 -10
  75. package/dist/core/templates/agents-template.d.ts +1 -1
  76. package/dist/core/templates/agents-template.js +510 -243
  77. package/dist/core/templates/index.d.ts +1 -0
  78. package/dist/core/templates/index.js +1 -0
  79. package/dist/core/templates/skill-templates.js +46 -152
  80. package/dist/core/templates/slash-command-templates.d.ts +1 -1
  81. package/dist/core/templates/slash-command-templates.js +352 -20
  82. package/dist/core/templates/tasks-template.d.ts +7 -0
  83. package/dist/core/templates/tasks-template.js +130 -24
  84. package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
  85. package/dist/core/templates/tdd-tasks-template.js +91 -38
  86. package/dist/core/templates/test-cases-template.d.ts +41 -0
  87. package/dist/core/templates/test-cases-template.js +128 -0
  88. package/dist/core/validation/strict-rules.d.ts +44 -5
  89. package/dist/core/validation/strict-rules.js +302 -8
  90. package/dist/core/validation/validator.js +52 -2
  91. package/dist/core/view.d.ts +1 -0
  92. package/dist/core/view.js +60 -2
  93. package/dist/mcp/index.d.ts +28 -0
  94. package/dist/mcp/index.js +31 -0
  95. package/dist/utils/file-system.d.ts +1 -0
  96. package/dist/utils/file-system.js +11 -0
  97. package/dist/utils/item-discovery.js +24 -2
  98. package/dist/utils/phase-utils.d.ts +36 -0
  99. package/dist/utils/phase-utils.js +117 -0
  100. package/package.json +22 -23
  101. package/schemas/spec-driven/schema.yaml +45 -31
  102. package/schemas/spec-driven/templates/spec.md +142 -5
  103. package/schemas/spec-driven/templates/tasks.md +73 -9
@@ -0,0 +1,472 @@
1
+ import { execSync } from 'child_process';
2
+ import { promises as fs, readdirSync, existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { FileSystemUtils } from '../utils/file-system.js';
5
+ import { mcp__sonar__get_Sonar_Query } from '../mcp/index.js';
6
+ import { writeCodeReviewOutput, writeUnitTestOutput, writeSpecConsistencyOutput, codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
7
+ export class ReviewCommand {
8
+ async execute(changeName, options) {
9
+ if (!changeName) {
10
+ const selected = await this.selectChangeInteractively();
11
+ if (!selected) {
12
+ throw new Error('No change selected. Usage: zhuanspec review <change-id>');
13
+ }
14
+ changeName = selected;
15
+ }
16
+ const repoRoot = process.cwd();
17
+ const changeDir = path.join(repoRoot, 'zhuanspec', 'changes', changeName);
18
+ const metricsDir = path.join(changeDir, 'metrics');
19
+ const proposalPath = path.join(changeDir, 'proposal.md');
20
+ const tasksPath = path.join(changeDir, 'tasks.md');
21
+ const changeSpecsDir = path.join(changeDir, 'specs');
22
+ const proposalExists = await FileSystemUtils.fileExists(proposalPath);
23
+ const tasksExists = await FileSystemUtils.fileExists(tasksPath);
24
+ if (!proposalExists || !tasksExists) {
25
+ throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
26
+ }
27
+ const issues = [];
28
+ const requirementNames = await this.collectRequirementNames(changeSpecsDir);
29
+ const touchedFiles = await this.collectTouchedFiles(repoRoot);
30
+ const touchedContent = await this.collectTouchedFileContent(repoRoot, touchedFiles);
31
+ // Track whether every requirement has at least one weak signal in changed code.
32
+ for (const req of requirementNames) {
33
+ const normalized = req.toLowerCase();
34
+ const matched = touchedContent.some((content) => content.includes(normalized));
35
+ if (!matched) {
36
+ issues.push({
37
+ severity: 'important',
38
+ message: `Requirement may not be mapped to implementation: ${req}`,
39
+ source: 'specCompliance',
40
+ });
41
+ }
42
+ }
43
+ const tasksContent = await fs.readFile(tasksPath, 'utf-8');
44
+ const incompleteTasks = (tasksContent.match(/^-\s*\[\s\]\s+/gm) ?? []).length;
45
+ if (incompleteTasks > 0) {
46
+ issues.push({
47
+ severity: 'critical',
48
+ message: `Found ${incompleteTasks} incomplete tasks in tasks.md`,
49
+ source: 'specCompliance',
50
+ });
51
+ }
52
+ // Very lightweight code-quality checks.
53
+ for (const file of touchedFiles) {
54
+ if (file.endsWith('.ts') || (file.endsWith('.js') && !file.includes('node_modules'))) {
55
+ const content = await fs.readFile(path.join(repoRoot, file), 'utf-8');
56
+ if (content.includes('TODO') || content.includes('FIXME')) {
57
+ issues.push({
58
+ severity: 'minor',
59
+ message: `Found TODO/FIXME in ${file}`,
60
+ source: 'codeQuality',
61
+ });
62
+ }
63
+ }
64
+ }
65
+ // Sonar MCP integration - fetch issues from SonarQube
66
+ const sonarIssues = await this.fetchSonarIssues(repoRoot);
67
+ for (const issue of sonarIssues) {
68
+ const severity = issue.type === 'BUG' || issue.type === 'VULNERABILITY' ? 'critical' : 'important';
69
+ issues.push({
70
+ severity,
71
+ message: `Sonar ${issue.type}: ${issue.message} (${issue.file}${issue.line ? ':' + issue.line : ''})`,
72
+ source: 'codeQuality',
73
+ });
74
+ }
75
+ // Scenario→Test mapping validation
76
+ const scenarios = await this.collectScenarios(changeSpecsDir);
77
+ const testFiles = await this.findTestFiles(repoRoot);
78
+ for (const scenario of scenarios) {
79
+ const testPatterns = this.scenarioToTestMethodNames(scenario.name);
80
+ const matched = testFiles.some(tf => testPatterns.some(p => tf.toLowerCase().includes(p.toLowerCase())));
81
+ if (!matched) {
82
+ issues.push({
83
+ severity: 'important',
84
+ message: `Scenario "${scenario.name}" 未找到对应测试文件`,
85
+ source: 'specCompliance',
86
+ });
87
+ }
88
+ }
89
+ const testCommand = options?.testCommand?.trim() || 'npm test';
90
+ const testResult = this.runTests(repoRoot, testCommand);
91
+ const coverageThreshold = Number(options?.coverageThreshold ?? 80);
92
+ if (testResult.coverage !== null && testResult.coverage < coverageThreshold) {
93
+ issues.push({
94
+ severity: 'important',
95
+ message: `Coverage ${testResult.coverage}% is below threshold ${coverageThreshold}%`,
96
+ source: 'unitTests',
97
+ });
98
+ }
99
+ if (testResult.failed > 0) {
100
+ issues.push({
101
+ severity: 'critical',
102
+ message: `Unit tests failed: ${testResult.failed}`,
103
+ source: 'unitTests',
104
+ });
105
+ }
106
+ const summary = this.buildSummary(issues, testResult, testCommand);
107
+ const consistency = this.buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles);
108
+ // Persist skill-compatible outputs so hooks and command share one result chain.
109
+ await writeCodeReviewOutput(changeName, {
110
+ criticalCount: summary.summary.critical,
111
+ importantCount: summary.summary.important,
112
+ infoCount: summary.summary.minor,
113
+ issues: summary.issues.map((issue) => ({
114
+ level: issue.severity === 'minor' ? 'info' : issue.severity,
115
+ file: issue.source,
116
+ message: issue.message,
117
+ })),
118
+ sonarStatus: sonarIssues.length > 0 ? 'pass' : 'skip',
119
+ loopCount: 0,
120
+ });
121
+ await writeUnitTestOutput(changeName, {
122
+ testPassed: testResult.failed === 0,
123
+ passRate: testResult.failed === 0 ? 100 : 0,
124
+ coverage: testResult.coverage ?? 0,
125
+ coverageThreshold,
126
+ newTestsGenerated: [],
127
+ failedTests: testResult.failed > 0 ? ['test-command-failed'] : [],
128
+ loopCount: 0,
129
+ });
130
+ await writeSpecConsistencyOutput(changeName, {
131
+ consistencyRate: consistency.consistencyRate,
132
+ totalRequirements: consistency.totalRequirements,
133
+ totalScenarios: consistency.totalScenarios,
134
+ coveredScenarios: consistency.coveredScenarios,
135
+ uncoveredScenarios: consistency.uncoveredScenarios.map((scenario, idx) => ({
136
+ requirementId: `REQ-${String(idx + 1).padStart(3, '0')}`,
137
+ scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
138
+ description: scenario.name,
139
+ })),
140
+ mapping: consistency.mapping,
141
+ loopCount: 0,
142
+ });
143
+ // Build unified review-report.md from hook checkers.
144
+ const codeReviewResult = await codeReviewResultCheck(changeName);
145
+ const unitTestResult = await unitTestResultCheck(changeName);
146
+ const specConsistencyResult = await specConsistencyResultCheck(changeName);
147
+ await generateReviewReport(changeName, {
148
+ codeReview: codeReviewResult,
149
+ unitTest: unitTestResult,
150
+ specConsistency: specConsistencyResult,
151
+ });
152
+ await FileSystemUtils.createDirectory(metricsDir);
153
+ await FileSystemUtils.writeFile(path.join(metricsDir, 'review.json'), JSON.stringify(summary, null, 2));
154
+ await FileSystemUtils.writeFile(path.join(metricsDir, 'tests.json'), JSON.stringify(testResult, null, 2));
155
+ if (options?.json) {
156
+ console.log(JSON.stringify(summary, null, 2));
157
+ }
158
+ else {
159
+ this.printSummary(changeName, summary);
160
+ }
161
+ process.exitCode = summary.passed ? 0 : 1;
162
+ }
163
+ async selectChangeInteractively() {
164
+ const changesDir = path.join(process.cwd(), 'zhuanspec', 'changes');
165
+ let entries = [];
166
+ try {
167
+ entries = await fs.readdir(changesDir, { withFileTypes: true });
168
+ }
169
+ catch {
170
+ throw new Error("No zhuanspec changes directory found. Run 'zhuanspec init' first.");
171
+ }
172
+ const changes = entries
173
+ .filter((entry) => entry.isDirectory() && entry.name !== 'archive' && !entry.name.startsWith('.'))
174
+ .map((entry) => entry.name)
175
+ .sort((a, b) => a.localeCompare(b));
176
+ if (changes.length === 0) {
177
+ throw new Error('No active changes found. Create one first with zhuanspec new change <name>.');
178
+ }
179
+ if (!process.stdin.isTTY) {
180
+ throw new Error(`Missing change name. Available changes: ${changes.join(', ')}`);
181
+ }
182
+ const { select } = await import('@inquirer/prompts');
183
+ try {
184
+ return await select({
185
+ message: '选择要执行 review 的变更',
186
+ choices: changes.map((id) => ({ name: id, value: id })),
187
+ });
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ async collectRequirementNames(changeSpecsDir) {
194
+ const requirements = [];
195
+ const walk = async (dir) => {
196
+ let entries = [];
197
+ try {
198
+ entries = await fs.readdir(dir, { withFileTypes: true });
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ for (const entry of entries) {
204
+ const full = path.join(dir, entry.name);
205
+ if (entry.isDirectory()) {
206
+ await walk(full);
207
+ continue;
208
+ }
209
+ if (entry.isFile() && entry.name === 'spec.md') {
210
+ const content = await fs.readFile(full, 'utf-8');
211
+ const matches = content.match(/^###\s+(Requirement|需求):\s+(.+)$/gim) ?? [];
212
+ for (const line of matches) {
213
+ const parts = line.split(':');
214
+ const name = parts.slice(1).join(':').trim();
215
+ if (name)
216
+ requirements.push(name);
217
+ }
218
+ }
219
+ }
220
+ };
221
+ await walk(changeSpecsDir);
222
+ return Array.from(new Set(requirements));
223
+ }
224
+ async collectTouchedFiles(repoRoot) {
225
+ try {
226
+ const output = execSync('git diff --name-only', { cwd: repoRoot, encoding: 'utf-8' });
227
+ return output
228
+ .split('\n')
229
+ .map((item) => item.trim())
230
+ .filter((item) => item.length > 0 && !item.startsWith('zhuanspec/changes/archive/'));
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
236
+ async collectTouchedFileContent(repoRoot, files) {
237
+ const result = [];
238
+ for (const file of files) {
239
+ try {
240
+ const full = path.join(repoRoot, file);
241
+ const stat = await fs.stat(full);
242
+ if (!stat.isFile())
243
+ continue;
244
+ const content = await fs.readFile(full, 'utf-8');
245
+ result.push(content.toLowerCase());
246
+ }
247
+ catch {
248
+ // ignore unreadable files
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ runTests(repoRoot, command) {
254
+ try {
255
+ const output = execSync(command, { cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
256
+ const coverageMatch = output.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
257
+ return {
258
+ passed: 1,
259
+ failed: 0,
260
+ coverage: coverageMatch ? Number(coverageMatch[1]) : null,
261
+ };
262
+ }
263
+ catch (error) {
264
+ const stdout = error?.stdout?.toString?.() ?? '';
265
+ const stderr = error?.stderr?.toString?.() ?? '';
266
+ const combined = `${stdout}\n${stderr}`;
267
+ const coverageMatch = combined.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
268
+ return {
269
+ passed: 0,
270
+ failed: 1,
271
+ coverage: coverageMatch ? Number(coverageMatch[1]) : null,
272
+ };
273
+ }
274
+ }
275
+ buildSummary(issues, testResult, command) {
276
+ const critical = issues.filter((i) => i.severity === 'critical').length;
277
+ const important = issues.filter((i) => i.severity === 'important').length;
278
+ const minor = issues.filter((i) => i.severity === 'minor').length;
279
+ return {
280
+ passed: critical === 0 && testResult.failed === 0,
281
+ summary: { critical, important, minor },
282
+ checks: {
283
+ specCompliance: !issues.some((i) => i.source === 'specCompliance' && i.severity === 'critical'),
284
+ codeQuality: !issues.some((i) => i.source === 'codeQuality' && i.severity === 'critical'),
285
+ unitTests: testResult.failed === 0,
286
+ },
287
+ test: {
288
+ passed: testResult.passed,
289
+ failed: testResult.failed,
290
+ coverage: testResult.coverage,
291
+ command,
292
+ },
293
+ issues,
294
+ generatedAt: new Date().toISOString(),
295
+ };
296
+ }
297
+ buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles) {
298
+ const normalizedTouched = touchedFiles.map((file) => file.toLowerCase());
299
+ const normalizedTests = testFiles.map((file) => file.toLowerCase());
300
+ const mapping = [];
301
+ const uncoveredScenarios = [];
302
+ scenarios.forEach((scenario, idx) => {
303
+ const patterns = this.scenarioToTestMethodNames(scenario.name).map((p) => p.toLowerCase());
304
+ const testCovered = normalizedTests.some((testFile) => patterns.some((p) => testFile.includes(p)));
305
+ const codeCovered = normalizedTouched.some((codeFile) => patterns.some((p) => codeFile.includes(p)));
306
+ const status = testCovered && codeCovered
307
+ ? 'covered'
308
+ : (testCovered || codeCovered ? 'partial' : 'missing');
309
+ mapping.push({
310
+ requirementId: `REQ-${String((idx % Math.max(requirementNames.length, 1)) + 1).padStart(3, '0')}`,
311
+ scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
312
+ codeFile: touchedFiles[0] || '',
313
+ status,
314
+ });
315
+ if (status !== 'covered') {
316
+ uncoveredScenarios.push(scenario);
317
+ }
318
+ });
319
+ const totalScenarios = scenarios.length;
320
+ const coveredScenarios = totalScenarios - uncoveredScenarios.length;
321
+ const consistencyRate = totalScenarios === 0 ? 100 : Math.round((coveredScenarios / totalScenarios) * 100);
322
+ return {
323
+ consistencyRate,
324
+ totalRequirements: requirementNames.length,
325
+ totalScenarios,
326
+ coveredScenarios,
327
+ uncoveredScenarios,
328
+ mapping,
329
+ };
330
+ }
331
+ printSummary(changeName, summary) {
332
+ console.log(`Review report for ${changeName}`);
333
+ console.log(`Passed: ${summary.passed ? 'YES' : 'NO'}`);
334
+ console.log(`Issues: critical=${summary.summary.critical}, important=${summary.summary.important}, minor=${summary.summary.minor}`);
335
+ console.log(`Tests: passed=${summary.test.passed}, failed=${summary.test.failed}, coverage=${summary.test.coverage ?? 'N/A'}`);
336
+ if (summary.issues.length > 0) {
337
+ console.log('Findings:');
338
+ for (const item of summary.issues) {
339
+ console.log(`- [${item.severity}] (${item.source}) ${item.message}`);
340
+ }
341
+ }
342
+ }
343
+ /**
344
+ * Fetch Sonar issues using MCP
345
+ */
346
+ async fetchSonarIssues(repoRoot) {
347
+ try {
348
+ const branch = execSync('git branch --show-current', {
349
+ cwd: repoRoot,
350
+ encoding: 'utf-8',
351
+ stdio: ['pipe', 'pipe', 'ignore'],
352
+ }).trim();
353
+ // Query different types
354
+ const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
355
+ const allIssues = [];
356
+ for (const type of types) {
357
+ try {
358
+ const result = await mcp__sonar__get_Sonar_Query({
359
+ branchName: branch,
360
+ staticType: type,
361
+ });
362
+ if (result && Array.isArray(result)) {
363
+ for (const issue of result) {
364
+ allIssues.push({
365
+ type: type,
366
+ severity: issue.severity || 'MAJOR',
367
+ message: issue.message || '',
368
+ file: issue.file || '',
369
+ line: issue.line,
370
+ });
371
+ }
372
+ }
373
+ }
374
+ catch {
375
+ // Sonar MCP might not be configured, skip silently
376
+ }
377
+ }
378
+ return allIssues;
379
+ }
380
+ catch {
381
+ // Git command failed or Sonar not available
382
+ return [];
383
+ }
384
+ }
385
+ /**
386
+ * Collect all Scenarios from delta specs
387
+ */
388
+ async collectScenarios(changeSpecsDir) {
389
+ const scenarios = [];
390
+ const walk = async (dir) => {
391
+ let entries = [];
392
+ try {
393
+ entries = await fs.readdir(dir, { withFileTypes: true });
394
+ }
395
+ catch {
396
+ return;
397
+ }
398
+ for (const entry of entries) {
399
+ const full = path.join(dir, entry.name);
400
+ if (entry.isDirectory()) {
401
+ await walk(full);
402
+ continue;
403
+ }
404
+ if (entry.isFile() && entry.name === 'spec.md') {
405
+ const content = await fs.readFile(full, 'utf-8');
406
+ // Match Scenario headers
407
+ const matches = content.matchAll(/^####\s+(Scenario|场景)[::]\s+(.+)$/gim);
408
+ for (const match of matches) {
409
+ const name = match[2].trim();
410
+ scenarios.push({ name, specFile: full });
411
+ }
412
+ }
413
+ }
414
+ };
415
+ await walk(changeSpecsDir);
416
+ return scenarios;
417
+ }
418
+ /**
419
+ * Find test files in the project
420
+ */
421
+ async findTestFiles(repoRoot) {
422
+ const testFiles = [];
423
+ const testDirs = ['test', 'tests', 'src/test', 'spec', '__tests__'];
424
+ for (const testDir of testDirs) {
425
+ const fullDir = path.join(repoRoot, testDir);
426
+ try {
427
+ if (!existsSync(fullDir))
428
+ continue;
429
+ const walk = (dir) => {
430
+ const entries = readdirSync(dir, { withFileTypes: true });
431
+ for (const entry of entries) {
432
+ const full = path.join(dir, entry.name);
433
+ if (entry.isDirectory()) {
434
+ walk(full);
435
+ }
436
+ else if (entry.isFile() && (entry.name.endsWith('.test.ts') ||
437
+ entry.name.endsWith('.test.js') ||
438
+ entry.name.endsWith('.spec.ts') ||
439
+ entry.name.endsWith('.spec.js') ||
440
+ entry.name.includes('Test'))) {
441
+ testFiles.push(path.relative(repoRoot, full));
442
+ }
443
+ }
444
+ };
445
+ walk(fullDir);
446
+ }
447
+ catch {
448
+ // Directory doesn't exist or can't be read
449
+ }
450
+ }
451
+ return testFiles;
452
+ }
453
+ /**
454
+ * Convert Scenario name to possible test method names
455
+ */
456
+ scenarioToTestMethodNames(scenarioName) {
457
+ const name = scenarioName.replace(/[::]/g, '').trim();
458
+ return [
459
+ // snake_case
460
+ name.replace(/\s+/g, '_').toLowerCase(),
461
+ // camelCase
462
+ name.replace(/\s+/g, '').toLowerCase(),
463
+ // PascalCase
464
+ name.split(/\s+/).map(w => w[0]?.toUpperCase() + w.slice(1).toLowerCase()).join(''),
465
+ // Remove Chinese characters for keyword matching
466
+ name.replace(/[^\w\s]/g, '').trim().toLowerCase(),
467
+ // Key words from scenario name
468
+ ...name.split(/\s+/).filter(w => w.length > 3),
469
+ ].filter(n => n.length > 0);
470
+ }
471
+ }
472
+ //# sourceMappingURL=review.js.map
@@ -8,6 +8,7 @@ interface ExecuteOptions {
8
8
  noInteractive?: boolean;
9
9
  interactive?: boolean;
10
10
  concurrency?: string;
11
+ autoFix?: boolean;
11
12
  }
12
13
  export declare class ValidateCommand {
13
14
  execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
@@ -16,6 +17,19 @@ export declare class ValidateCommand {
16
17
  private printNonInteractiveHint;
17
18
  private validateDirectItem;
18
19
  private validateByType;
20
+ /**
21
+ * Attempt to auto-fix common validation issues.
22
+ * Returns true if any fixes were applied.
23
+ */
24
+ private tryAutoFix;
25
+ /**
26
+ * Add a section skeleton to proposal.md
27
+ */
28
+ private addProposalSectionSkeleton;
29
+ /**
30
+ * Add review gate section to tasks.md
31
+ */
32
+ private addReviewGateToTasks;
19
33
  private printReport;
20
34
  private printNextSteps;
21
35
  /**