@zhuan-ai/zhuanspec 2.9.5 → 2.11.2
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/hooks.js +8 -8
- package/dist/cli/index.js +1 -0
- package/dist/commands/design.d.ts +12 -2
- package/dist/commands/design.js +83 -10
- package/dist/commands/progress.js +11 -0
- package/dist/commands/review.d.ts +1 -25
- package/dist/commands/review.js +51 -408
- package/dist/core/completions/command-registry.js +4 -0
- package/dist/core/hooks/collect-knowledge.d.ts +13 -5
- package/dist/core/hooks/collect-knowledge.js +56 -164
- package/dist/core/hooks/deviation-check.js +159 -55
- package/dist/core/hooks/init.js +46 -0
- package/dist/core/hooks/post-apply.js +2 -0
- package/dist/core/hooks/pre-archive.js +13 -14
- package/dist/core/hooks/pre-review.d.ts +27 -0
- package/dist/core/hooks/pre-review.js +169 -0
- package/dist/core/hooks/record-progress.d.ts +25 -0
- package/dist/core/hooks/record-progress.js +88 -1
- package/dist/core/hooks/review-hooks.js +176 -7
- package/dist/core/hooks/review-orchestrator.js +58 -11
- package/dist/core/hooks/tdd-phase-hook.d.ts +59 -0
- package/dist/core/hooks/tdd-phase-hook.js +313 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +45 -2
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +139 -23
- package/dist/core/templates/skill-templates.js +21 -1
- package/dist/core/templates/slash-command-templates.js +163 -63
- package/dist/core/templates/tasks-template.js +89 -0
- package/dist/core/update.d.ts +3 -0
- package/dist/core/update.js +85 -2
- package/dist/core/validation/strict-rules.d.ts +47 -0
- package/dist/core/validation/strict-rules.js +589 -42
- package/dist/utils/git-repo-detector.js +1 -1
- package/dist/utils/phase-utils.js +14 -1
- package/package.json +22 -20
package/dist/commands/review.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promises as fs, readdirSync, existsSync } from 'fs';
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
|
-
import {
|
|
6
|
-
import { mcp__sonar__get_Sonar_Query } from '../mcp/index.js';
|
|
7
|
-
import { writeCodeReviewOutput, writeUnitTestOutput, writeSpecConsistencyOutput, codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
|
|
4
|
+
import { codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
|
|
8
5
|
export class ReviewCommand {
|
|
9
6
|
async execute(changeName, options) {
|
|
10
7
|
if (!changeName) {
|
|
@@ -16,136 +13,46 @@ export class ReviewCommand {
|
|
|
16
13
|
}
|
|
17
14
|
const cwd = process.cwd();
|
|
18
15
|
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeName);
|
|
19
|
-
const
|
|
16
|
+
const reviewDir = path.join(changeDir, 'review');
|
|
20
17
|
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
21
18
|
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
22
|
-
|
|
19
|
+
// Check proposal and tasks exist
|
|
23
20
|
const proposalExists = await FileSystemUtils.fileExists(proposalPath);
|
|
24
21
|
const tasksExists = await FileSystemUtils.fileExists(tasksPath);
|
|
25
22
|
if (!proposalExists || !tasksExists) {
|
|
26
23
|
throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
|
|
27
24
|
}
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (file.endsWith('.ts') || (file.endsWith('.js') && !file.includes('node_modules'))) {
|
|
60
|
-
const content = await fs.readFile(file, 'utf-8');
|
|
61
|
-
if (content.includes('TODO') || content.includes('FIXME')) {
|
|
62
|
-
issues.push({
|
|
63
|
-
severity: 'minor',
|
|
64
|
-
message: `Found TODO/FIXME in ${file}`,
|
|
65
|
-
source: 'codeQuality',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Sonar MCP integration - fetch issues from SonarQube (use first detected repo if available)
|
|
71
|
-
const sonarIssues = await this.fetchSonarIssues(allRepos);
|
|
72
|
-
for (const issue of sonarIssues) {
|
|
73
|
-
const severity = issue.type === 'BUG' || issue.type === 'VULNERABILITY' ? 'critical' : 'important';
|
|
74
|
-
issues.push({
|
|
75
|
-
severity,
|
|
76
|
-
message: `Sonar ${issue.type}: ${issue.message} (${issue.file}${issue.line ? ':' + issue.line : ''})`,
|
|
77
|
-
source: 'codeQuality',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
// Scenario→Test mapping validation
|
|
81
|
-
const scenarios = await this.collectScenarios(changeSpecsDir);
|
|
82
|
-
const testFiles = await this.findTestFiles(cwd);
|
|
83
|
-
for (const scenario of scenarios) {
|
|
84
|
-
const testPatterns = this.scenarioToTestMethodNames(scenario.name);
|
|
85
|
-
const matched = testFiles.some(tf => testPatterns.some(p => tf.toLowerCase().includes(p.toLowerCase())));
|
|
86
|
-
if (!matched) {
|
|
87
|
-
issues.push({
|
|
88
|
-
severity: 'important',
|
|
89
|
-
message: `Scenario "${scenario.name}" 未找到对应测试文件`,
|
|
90
|
-
source: 'specCompliance',
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const testCommand = options?.testCommand?.trim() || 'npm test';
|
|
95
|
-
const testResult = this.runTests(cwd, testCommand);
|
|
96
|
-
const coverageThreshold = Number(options?.coverageThreshold ?? 80);
|
|
97
|
-
if (testResult.coverage !== null && testResult.coverage < coverageThreshold) {
|
|
98
|
-
issues.push({
|
|
99
|
-
severity: 'important',
|
|
100
|
-
message: `Coverage ${testResult.coverage}% is below threshold ${coverageThreshold}%`,
|
|
101
|
-
source: 'unitTests',
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (testResult.failed > 0) {
|
|
105
|
-
issues.push({
|
|
106
|
-
severity: 'critical',
|
|
107
|
-
message: `Unit tests failed: ${testResult.failed}`,
|
|
108
|
-
source: 'unitTests',
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const summary = this.buildSummary(issues, testResult, testCommand);
|
|
112
|
-
const consistency = this.buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles);
|
|
113
|
-
// Persist skill-compatible outputs so hooks and command share one result chain.
|
|
114
|
-
await writeCodeReviewOutput(changeName, {
|
|
115
|
-
criticalCount: summary.summary.critical,
|
|
116
|
-
importantCount: summary.summary.important,
|
|
117
|
-
infoCount: summary.summary.minor,
|
|
118
|
-
issues: summary.issues.map((issue) => ({
|
|
119
|
-
level: issue.severity === 'minor' ? 'info' : issue.severity,
|
|
120
|
-
file: issue.source,
|
|
121
|
-
message: issue.message,
|
|
122
|
-
})),
|
|
123
|
-
sonarStatus: sonarIssues.length > 0 ? 'pass' : 'skip',
|
|
124
|
-
loopCount: 0,
|
|
125
|
-
});
|
|
126
|
-
await writeUnitTestOutput(changeName, {
|
|
127
|
-
testPassed: testResult.failed === 0,
|
|
128
|
-
passRate: testResult.failed === 0 ? 100 : 0,
|
|
129
|
-
coverage: testResult.coverage ?? 0,
|
|
130
|
-
coverageThreshold,
|
|
131
|
-
newTestsGenerated: [],
|
|
132
|
-
failedTests: testResult.failed > 0 ? ['test-command-failed'] : [],
|
|
133
|
-
loopCount: 0,
|
|
134
|
-
});
|
|
135
|
-
await writeSpecConsistencyOutput(changeName, {
|
|
136
|
-
consistencyRate: consistency.consistencyRate,
|
|
137
|
-
totalRequirements: consistency.totalRequirements,
|
|
138
|
-
totalScenarios: consistency.totalScenarios,
|
|
139
|
-
coveredScenarios: consistency.coveredScenarios,
|
|
140
|
-
uncoveredScenarios: consistency.uncoveredScenarios.map((scenario, idx) => ({
|
|
141
|
-
requirementId: `REQ-${String(idx + 1).padStart(3, '0')}`,
|
|
142
|
-
scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
|
|
143
|
-
description: scenario.name,
|
|
144
|
-
})),
|
|
145
|
-
mapping: consistency.mapping,
|
|
146
|
-
loopCount: 0,
|
|
147
|
-
});
|
|
148
|
-
// Build unified review-report.md from hook checkers.
|
|
25
|
+
// === Task completion hard gate ===
|
|
26
|
+
if (tasksExists) {
|
|
27
|
+
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
28
|
+
const uncompletedTasks = tasksContent.match(/^- \[ \] .+$/gm) || [];
|
|
29
|
+
if (uncompletedTasks.length > 0 && !options?.force) {
|
|
30
|
+
throw new Error(`Change '${changeName}' has ${uncompletedTasks.length} uncompleted tasks. ` +
|
|
31
|
+
`Please complete all tasks before review. Use --force to override.\n` +
|
|
32
|
+
uncompletedTasks.map(t => ` ${t.trim()}`).join('\n'));
|
|
33
|
+
}
|
|
34
|
+
if (uncompletedTasks.length > 0 && options?.force) {
|
|
35
|
+
console.log(`⚠️ Force mode: skipping ${uncompletedTasks.length} uncompleted tasks`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Check skill output JSON files exist
|
|
39
|
+
const codeReviewJsonPath = path.join(reviewDir, 'code-review-result.json');
|
|
40
|
+
const unitTestJsonPath = path.join(reviewDir, 'unit-test-result.json');
|
|
41
|
+
const missingFiles = [];
|
|
42
|
+
if (!(await FileSystemUtils.fileExists(codeReviewJsonPath))) {
|
|
43
|
+
missingFiles.push('code-review-result.json(需先运行 code-review-expert skill)');
|
|
44
|
+
}
|
|
45
|
+
if (!(await FileSystemUtils.fileExists(unitTestJsonPath))) {
|
|
46
|
+
missingFiles.push('unit-test-result.json(需先运行 generate-mockito-unit-test skill)');
|
|
47
|
+
}
|
|
48
|
+
if (missingFiles.length > 0) {
|
|
49
|
+
console.error(`❌ skill 输出缺失,请先执行以下步骤:`);
|
|
50
|
+
missingFiles.forEach((f, i) => console.error(` ${i + 1}. ${f}`));
|
|
51
|
+
console.error(` ${missingFiles.length + 1}. spec-consistency-result.json(运行 zhuanspec validate <id> --strict 后由 AI 写入)`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Read hook results and generate report
|
|
149
56
|
const codeReviewResult = await codeReviewResultCheck(changeName);
|
|
150
57
|
const unitTestResult = await unitTestResultCheck(changeName);
|
|
151
58
|
const specConsistencyResult = await specConsistencyResultCheck(changeName);
|
|
@@ -154,16 +61,25 @@ export class ReviewCommand {
|
|
|
154
61
|
unitTest: unitTestResult,
|
|
155
62
|
specConsistency: specConsistencyResult,
|
|
156
63
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await FileSystemUtils.writeFile(path.join(metricsDir, 'tests.json'), JSON.stringify(testResult, null, 2));
|
|
64
|
+
// Output results
|
|
65
|
+
const overallPass = codeReviewResult.pass && unitTestResult.pass && specConsistencyResult.pass;
|
|
160
66
|
if (options?.json) {
|
|
161
|
-
console.log(JSON.stringify(
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
passed: overallPass,
|
|
69
|
+
codeReview: codeReviewResult,
|
|
70
|
+
unitTest: unitTestResult,
|
|
71
|
+
specConsistency: specConsistencyResult,
|
|
72
|
+
}, null, 2));
|
|
162
73
|
}
|
|
163
74
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
75
|
+
console.log(`Review report for ${changeName}`);
|
|
76
|
+
console.log(`Overall: ${overallPass ? 'PASS ✅' : 'FAIL ❌'}`);
|
|
77
|
+
console.log(`Code Review: ${codeReviewResult.pass ? 'PASS' : 'FAIL'} (critical=${codeReviewResult.metrics.criticalCount ?? 0})`);
|
|
78
|
+
console.log(`Unit Test: ${unitTestResult.pass ? 'PASS' : 'FAIL'} (coverage=${unitTestResult.metrics.coverage ?? 0}%)`);
|
|
79
|
+
console.log(`Spec-Code: ${specConsistencyResult.pass ? 'PASS' : 'FAIL'} (consistency=${specConsistencyResult.metrics.consistencyRate ?? 0}%)`);
|
|
80
|
+
console.log(`Report: ${path.join('zhuanspec/changes', changeName, 'review/review-report.md')}`);
|
|
81
|
+
}
|
|
82
|
+
process.exitCode = overallPass ? 0 : 1;
|
|
167
83
|
}
|
|
168
84
|
async selectChangeInteractively() {
|
|
169
85
|
const changesDir = path.join(process.cwd(), 'zhuanspec', 'changes');
|
|
@@ -195,278 +111,5 @@ export class ReviewCommand {
|
|
|
195
111
|
return null;
|
|
196
112
|
}
|
|
197
113
|
}
|
|
198
|
-
async collectRequirementNames(changeSpecsDir) {
|
|
199
|
-
const requirements = [];
|
|
200
|
-
const walk = async (dir) => {
|
|
201
|
-
let entries = [];
|
|
202
|
-
try {
|
|
203
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
for (const entry of entries) {
|
|
209
|
-
const full = path.join(dir, entry.name);
|
|
210
|
-
if (entry.isDirectory()) {
|
|
211
|
-
await walk(full);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (entry.isFile() && entry.name === 'spec.md') {
|
|
215
|
-
const content = await fs.readFile(full, 'utf-8');
|
|
216
|
-
const matches = content.match(/^###\s+(Requirement|需求):\s+(.+)$/gim) ?? [];
|
|
217
|
-
for (const line of matches) {
|
|
218
|
-
const parts = line.split(':');
|
|
219
|
-
const name = parts.slice(1).join(':').trim();
|
|
220
|
-
if (name)
|
|
221
|
-
requirements.push(name);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
await walk(changeSpecsDir);
|
|
227
|
-
return Array.from(new Set(requirements));
|
|
228
|
-
}
|
|
229
|
-
async collectTouchedFileContent(repoRoot, files) {
|
|
230
|
-
const result = [];
|
|
231
|
-
for (const file of files) {
|
|
232
|
-
try {
|
|
233
|
-
const full = path.isAbsolute(file) ? file : path.join(repoRoot, file);
|
|
234
|
-
const stat = await fs.stat(full);
|
|
235
|
-
if (!stat.isFile())
|
|
236
|
-
continue;
|
|
237
|
-
const content = await fs.readFile(full, 'utf-8');
|
|
238
|
-
result.push(content.toLowerCase());
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
// ignore unreadable files
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
|
-
runTests(repoRoot, command) {
|
|
247
|
-
try {
|
|
248
|
-
const output = execSync(command, { cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
249
|
-
const coverageMatch = output.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
|
|
250
|
-
return {
|
|
251
|
-
passed: 1,
|
|
252
|
-
failed: 0,
|
|
253
|
-
coverage: coverageMatch ? Number(coverageMatch[1]) : null,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
const stdout = error?.stdout?.toString?.() ?? '';
|
|
258
|
-
const stderr = error?.stderr?.toString?.() ?? '';
|
|
259
|
-
const combined = `${stdout}\n${stderr}`;
|
|
260
|
-
const coverageMatch = combined.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
|
|
261
|
-
return {
|
|
262
|
-
passed: 0,
|
|
263
|
-
failed: 1,
|
|
264
|
-
coverage: coverageMatch ? Number(coverageMatch[1]) : null,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
buildSummary(issues, testResult, command) {
|
|
269
|
-
const critical = issues.filter((i) => i.severity === 'critical').length;
|
|
270
|
-
const important = issues.filter((i) => i.severity === 'important').length;
|
|
271
|
-
const minor = issues.filter((i) => i.severity === 'minor').length;
|
|
272
|
-
return {
|
|
273
|
-
passed: critical === 0 && testResult.failed === 0,
|
|
274
|
-
summary: { critical, important, minor },
|
|
275
|
-
checks: {
|
|
276
|
-
specCompliance: !issues.some((i) => i.source === 'specCompliance' && i.severity === 'critical'),
|
|
277
|
-
codeQuality: !issues.some((i) => i.source === 'codeQuality' && i.severity === 'critical'),
|
|
278
|
-
unitTests: testResult.failed === 0,
|
|
279
|
-
},
|
|
280
|
-
test: {
|
|
281
|
-
passed: testResult.passed,
|
|
282
|
-
failed: testResult.failed,
|
|
283
|
-
coverage: testResult.coverage,
|
|
284
|
-
command,
|
|
285
|
-
},
|
|
286
|
-
issues,
|
|
287
|
-
generatedAt: new Date().toISOString(),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles) {
|
|
291
|
-
const normalizedTouched = touchedFiles.map((file) => file.toLowerCase());
|
|
292
|
-
const normalizedTests = testFiles.map((file) => file.toLowerCase());
|
|
293
|
-
const mapping = [];
|
|
294
|
-
const uncoveredScenarios = [];
|
|
295
|
-
scenarios.forEach((scenario, idx) => {
|
|
296
|
-
const patterns = this.scenarioToTestMethodNames(scenario.name).map((p) => p.toLowerCase());
|
|
297
|
-
const testCovered = normalizedTests.some((testFile) => patterns.some((p) => testFile.includes(p)));
|
|
298
|
-
const codeCovered = normalizedTouched.some((codeFile) => patterns.some((p) => codeFile.includes(p)));
|
|
299
|
-
const status = testCovered && codeCovered
|
|
300
|
-
? 'covered'
|
|
301
|
-
: (testCovered || codeCovered ? 'partial' : 'missing');
|
|
302
|
-
mapping.push({
|
|
303
|
-
requirementId: `REQ-${String((idx % Math.max(requirementNames.length, 1)) + 1).padStart(3, '0')}`,
|
|
304
|
-
scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
|
|
305
|
-
codeFile: touchedFiles[0] || '',
|
|
306
|
-
status,
|
|
307
|
-
});
|
|
308
|
-
if (status !== 'covered') {
|
|
309
|
-
uncoveredScenarios.push(scenario);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
const totalScenarios = scenarios.length;
|
|
313
|
-
const coveredScenarios = totalScenarios - uncoveredScenarios.length;
|
|
314
|
-
const consistencyRate = totalScenarios === 0 ? 100 : Math.round((coveredScenarios / totalScenarios) * 100);
|
|
315
|
-
return {
|
|
316
|
-
consistencyRate,
|
|
317
|
-
totalRequirements: requirementNames.length,
|
|
318
|
-
totalScenarios,
|
|
319
|
-
coveredScenarios,
|
|
320
|
-
uncoveredScenarios,
|
|
321
|
-
mapping,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
printSummary(changeName, summary) {
|
|
325
|
-
console.log(`Review report for ${changeName}`);
|
|
326
|
-
console.log(`Passed: ${summary.passed ? 'YES' : 'NO'}`);
|
|
327
|
-
console.log(`Issues: critical=${summary.summary.critical}, important=${summary.summary.important}, minor=${summary.summary.minor}`);
|
|
328
|
-
console.log(`Tests: passed=${summary.test.passed}, failed=${summary.test.failed}, coverage=${summary.test.coverage ?? 'N/A'}`);
|
|
329
|
-
if (summary.issues.length > 0) {
|
|
330
|
-
console.log('Findings:');
|
|
331
|
-
for (const item of summary.issues) {
|
|
332
|
-
console.log(`- [${item.severity}] (${item.source}) ${item.message}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Fetch Sonar issues using MCP
|
|
338
|
-
* Uses the first detected repository's branch for SonarQube queries
|
|
339
|
-
*/
|
|
340
|
-
async fetchSonarIssues(allRepos) {
|
|
341
|
-
// If no repos detected, skip Sonar queries
|
|
342
|
-
if (allRepos.length === 0) {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
try {
|
|
346
|
-
// Use the first detected repository's branch
|
|
347
|
-
const primaryRepo = allRepos[0];
|
|
348
|
-
const branch = execSync('git branch --show-current', {
|
|
349
|
-
cwd: primaryRepo,
|
|
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
114
|
}
|
|
472
115
|
//# sourceMappingURL=review.js.map
|
|
@@ -124,6 +124,10 @@ export const COMMAND_REGISTRY = [
|
|
|
124
124
|
positionalType: 'change-id',
|
|
125
125
|
flags: [
|
|
126
126
|
COMMON_FLAGS.json,
|
|
127
|
+
{
|
|
128
|
+
name: 'force',
|
|
129
|
+
description: 'Skip task completion check and force review',
|
|
130
|
+
},
|
|
127
131
|
{
|
|
128
132
|
name: 'test-command',
|
|
129
133
|
description: 'Test command to execute (default: npm test)',
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostToolUse Hook - Collect Knowledge
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Fires after Write/Edit in Apply phase. Injects a lightweight prompt
|
|
5
|
+
* into Claude's context so the LLM can decide (with user confirmation)
|
|
6
|
+
* whether the change is worth capturing as project knowledge.
|
|
7
|
+
*
|
|
8
|
+
* Design principle: no keyword matching, no auto-save — the LLM decides.
|
|
8
9
|
*/
|
|
9
10
|
interface CollectKnowledgeOptions {
|
|
10
11
|
json?: boolean;
|
|
11
12
|
file?: string;
|
|
12
|
-
|
|
13
|
+
}
|
|
14
|
+
interface HookOutput {
|
|
15
|
+
continue: boolean;
|
|
16
|
+
systemMessage?: string;
|
|
17
|
+
hookSpecificOutput?: {
|
|
18
|
+
additionalContext?: string;
|
|
19
|
+
};
|
|
13
20
|
}
|
|
14
21
|
export declare function collectKnowledgeHook(options: CollectKnowledgeOptions): Promise<void>;
|
|
22
|
+
export declare function runCollectKnowledge(filePath: string): Promise<HookOutput>;
|
|
15
23
|
export {};
|
|
16
24
|
//# sourceMappingURL=collect-knowledge.d.ts.map
|