@zhuan-ai/zhuanspec 2.6.0 → 2.9.5
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/commands/review.d.ts +1 -1
- package/dist/commands/review.js +24 -24
- package/dist/commands/validate.d.ts +0 -4
- package/dist/commands/validate.js +0 -22
- package/dist/core/archive.js +22 -13
- package/dist/core/hooks/deviation-check.js +2 -30
- package/dist/core/hooks/deviation-handler.d.ts +1 -0
- package/dist/core/hooks/deviation-handler.js +68 -15
- package/dist/core/hooks/post-apply.js +102 -39
- package/dist/core/hooks/record-progress.d.ts +167 -3
- package/dist/core/hooks/record-progress.js +144 -32
- package/dist/core/hooks/summarize.js +29 -10
- package/dist/core/init.js +11 -7
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +1 -1
- package/dist/core/templates/slash-command-templates.js +1 -1
- package/dist/core/validation/strict-rules.js +10 -42
- package/dist/utils/git-repo-detector.d.ts +130 -0
- package/dist/utils/git-repo-detector.js +275 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/phase-utils.d.ts +3 -3
- package/dist/utils/phase-utils.js +133 -10
- package/package.json +1 -1
|
@@ -6,7 +6,6 @@ export declare class ReviewCommand {
|
|
|
6
6
|
}): Promise<void>;
|
|
7
7
|
private selectChangeInteractively;
|
|
8
8
|
private collectRequirementNames;
|
|
9
|
-
private collectTouchedFiles;
|
|
10
9
|
private collectTouchedFileContent;
|
|
11
10
|
private runTests;
|
|
12
11
|
private buildSummary;
|
|
@@ -14,6 +13,7 @@ export declare class ReviewCommand {
|
|
|
14
13
|
private printSummary;
|
|
15
14
|
/**
|
|
16
15
|
* Fetch Sonar issues using MCP
|
|
16
|
+
* Uses the first detected repository's branch for SonarQube queries
|
|
17
17
|
*/
|
|
18
18
|
private fetchSonarIssues;
|
|
19
19
|
/**
|
package/dist/commands/review.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
|
|
|
2
2
|
import { promises as fs, readdirSync, existsSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
5
|
+
import { GitRepoDetector } from '../utils/git-repo-detector.js';
|
|
5
6
|
import { mcp__sonar__get_Sonar_Query } from '../mcp/index.js';
|
|
6
7
|
import { writeCodeReviewOutput, writeUnitTestOutput, writeSpecConsistencyOutput, codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
|
|
7
8
|
export class ReviewCommand {
|
|
@@ -13,8 +14,8 @@ export class ReviewCommand {
|
|
|
13
14
|
}
|
|
14
15
|
changeName = selected;
|
|
15
16
|
}
|
|
16
|
-
const
|
|
17
|
-
const changeDir = path.join(
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeName);
|
|
18
19
|
const metricsDir = path.join(changeDir, 'metrics');
|
|
19
20
|
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
20
21
|
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
@@ -24,10 +25,13 @@ export class ReviewCommand {
|
|
|
24
25
|
if (!proposalExists || !tasksExists) {
|
|
25
26
|
throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
|
|
26
27
|
}
|
|
28
|
+
// Detect all git repositories (parent + children)
|
|
29
|
+
const allRepos = GitRepoDetector.detectAllGitRepos(cwd);
|
|
27
30
|
const issues = [];
|
|
28
31
|
const requirementNames = await this.collectRequirementNames(changeSpecsDir);
|
|
29
|
-
|
|
30
|
-
const
|
|
32
|
+
// Use GitRepoDetector to get all changed files across all detected repos
|
|
33
|
+
const touchedFiles = GitRepoDetector.getAllChanges(cwd).filter((file) => !file.startsWith('zhuanspec/changes/archive/'));
|
|
34
|
+
const touchedContent = await this.collectTouchedFileContent(cwd, touchedFiles);
|
|
31
35
|
// Track whether every requirement has at least one weak signal in changed code.
|
|
32
36
|
for (const req of requirementNames) {
|
|
33
37
|
const normalized = req.toLowerCase();
|
|
@@ -50,9 +54,10 @@ export class ReviewCommand {
|
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
// Very lightweight code-quality checks.
|
|
57
|
+
// touchedFiles now contains absolute paths from GitRepoDetector
|
|
53
58
|
for (const file of touchedFiles) {
|
|
54
59
|
if (file.endsWith('.ts') || (file.endsWith('.js') && !file.includes('node_modules'))) {
|
|
55
|
-
const content = await fs.readFile(
|
|
60
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
56
61
|
if (content.includes('TODO') || content.includes('FIXME')) {
|
|
57
62
|
issues.push({
|
|
58
63
|
severity: 'minor',
|
|
@@ -62,8 +67,8 @@ export class ReviewCommand {
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
// Sonar MCP integration - fetch issues from SonarQube
|
|
66
|
-
const sonarIssues = await this.fetchSonarIssues(
|
|
70
|
+
// Sonar MCP integration - fetch issues from SonarQube (use first detected repo if available)
|
|
71
|
+
const sonarIssues = await this.fetchSonarIssues(allRepos);
|
|
67
72
|
for (const issue of sonarIssues) {
|
|
68
73
|
const severity = issue.type === 'BUG' || issue.type === 'VULNERABILITY' ? 'critical' : 'important';
|
|
69
74
|
issues.push({
|
|
@@ -74,7 +79,7 @@ export class ReviewCommand {
|
|
|
74
79
|
}
|
|
75
80
|
// Scenario→Test mapping validation
|
|
76
81
|
const scenarios = await this.collectScenarios(changeSpecsDir);
|
|
77
|
-
const testFiles = await this.findTestFiles(
|
|
82
|
+
const testFiles = await this.findTestFiles(cwd);
|
|
78
83
|
for (const scenario of scenarios) {
|
|
79
84
|
const testPatterns = this.scenarioToTestMethodNames(scenario.name);
|
|
80
85
|
const matched = testFiles.some(tf => testPatterns.some(p => tf.toLowerCase().includes(p.toLowerCase())));
|
|
@@ -87,7 +92,7 @@ export class ReviewCommand {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
const testCommand = options?.testCommand?.trim() || 'npm test';
|
|
90
|
-
const testResult = this.runTests(
|
|
95
|
+
const testResult = this.runTests(cwd, testCommand);
|
|
91
96
|
const coverageThreshold = Number(options?.coverageThreshold ?? 80);
|
|
92
97
|
if (testResult.coverage !== null && testResult.coverage < coverageThreshold) {
|
|
93
98
|
issues.push({
|
|
@@ -221,23 +226,11 @@ export class ReviewCommand {
|
|
|
221
226
|
await walk(changeSpecsDir);
|
|
222
227
|
return Array.from(new Set(requirements));
|
|
223
228
|
}
|
|
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
229
|
async collectTouchedFileContent(repoRoot, files) {
|
|
237
230
|
const result = [];
|
|
238
231
|
for (const file of files) {
|
|
239
232
|
try {
|
|
240
|
-
const full = path.join(repoRoot, file);
|
|
233
|
+
const full = path.isAbsolute(file) ? file : path.join(repoRoot, file);
|
|
241
234
|
const stat = await fs.stat(full);
|
|
242
235
|
if (!stat.isFile())
|
|
243
236
|
continue;
|
|
@@ -342,11 +335,18 @@ export class ReviewCommand {
|
|
|
342
335
|
}
|
|
343
336
|
/**
|
|
344
337
|
* Fetch Sonar issues using MCP
|
|
338
|
+
* Uses the first detected repository's branch for SonarQube queries
|
|
345
339
|
*/
|
|
346
|
-
async fetchSonarIssues(
|
|
340
|
+
async fetchSonarIssues(allRepos) {
|
|
341
|
+
// If no repos detected, skip Sonar queries
|
|
342
|
+
if (allRepos.length === 0) {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
347
345
|
try {
|
|
346
|
+
// Use the first detected repository's branch
|
|
347
|
+
const primaryRepo = allRepos[0];
|
|
348
348
|
const branch = execSync('git branch --show-current', {
|
|
349
|
-
cwd:
|
|
349
|
+
cwd: primaryRepo,
|
|
350
350
|
encoding: 'utf-8',
|
|
351
351
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
352
352
|
}).trim();
|
|
@@ -26,10 +26,6 @@ export declare class ValidateCommand {
|
|
|
26
26
|
* Add a section skeleton to proposal.md
|
|
27
27
|
*/
|
|
28
28
|
private addProposalSectionSkeleton;
|
|
29
|
-
/**
|
|
30
|
-
* Add review gate section to tasks.md
|
|
31
|
-
*/
|
|
32
|
-
private addReviewGateToTasks;
|
|
33
29
|
private printReport;
|
|
34
30
|
private printNextSteps;
|
|
35
31
|
/**
|
|
@@ -189,12 +189,6 @@ export class ValidateCommand {
|
|
|
189
189
|
}
|
|
190
190
|
continue;
|
|
191
191
|
}
|
|
192
|
-
// 2. Missing review gate - add to tasks.md
|
|
193
|
-
if (issue.message.includes('review gate') || issue.message.includes('zhuanspec review')) {
|
|
194
|
-
await this.addReviewGateToTasks(changeDir);
|
|
195
|
-
fixed = true;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
192
|
// 4. Missing @skill tag - add @skill:none (existing logic)
|
|
199
193
|
if (issue.message.includes('Skill Mapping') || issue.message.includes('@skill')) {
|
|
200
194
|
const tasksFile = path.join(changeDir, 'tasks.md');
|
|
@@ -264,22 +258,6 @@ export class ValidateCommand {
|
|
|
264
258
|
// Cannot read/write proposal.md
|
|
265
259
|
}
|
|
266
260
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Add review gate section to tasks.md
|
|
269
|
-
*/
|
|
270
|
-
async addReviewGateToTasks(changeDir) {
|
|
271
|
-
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
272
|
-
try {
|
|
273
|
-
let content = await fs.readFile(tasksPath, 'utf-8');
|
|
274
|
-
if (!content.includes('zhuanspec review') && !content.includes('审查门禁')) {
|
|
275
|
-
content = content.trimEnd() + '\n\n## 审查门禁\n\n- [ ] 执行 `zhuanspec review <change-id>` 并确认通过\n';
|
|
276
|
-
await fs.writeFile(tasksPath, content, 'utf-8');
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
// Cannot read/write tasks.md
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
261
|
printReport(type, id, report, durationMs, json, taskReport, opts) {
|
|
284
262
|
if (json) {
|
|
285
263
|
const out = {
|
package/dist/core/archive.js
CHANGED
|
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
10
10
|
import { preArchiveHook } from './hooks/pre-archive.js';
|
|
11
11
|
import { postArchiveHook } from './hooks/post-archive.js';
|
|
12
|
+
import { GitRepoDetector } from '../utils/git-repo-detector.js';
|
|
12
13
|
const ARCH_REPO_URL = 'http://gitlab.zhuanspirit.com/zz-kf/spec_repo.git';
|
|
13
14
|
const ARCH_REPO_BRANCH = 'spec_repo-feature-6612-2';
|
|
14
15
|
export class ArchiveCommand {
|
|
@@ -25,12 +26,18 @@ export class ArchiveCommand {
|
|
|
25
26
|
catch {
|
|
26
27
|
throw new Error("未找到 ZhuanSpec 变更目录。请先运行 'zhuanspec init'。");
|
|
27
28
|
}
|
|
29
|
+
const skipSpecs = options.skipSpecs === true;
|
|
28
30
|
const businessDirection = await this.resolveBusinessDirection(zhuanspecDir, options.businessDirection);
|
|
29
|
-
if (!businessDirection) {
|
|
30
|
-
throw new Error('未解析到业务方向,无法执行归档前拉取与归档后回推。请使用 --business-direction 或在 zhuanspec/.business-direction、zhuanspec/project.md
|
|
31
|
+
if (!businessDirection && !skipSpecs) {
|
|
32
|
+
throw new Error('未解析到业务方向,无法执行归档前拉取与归档后回推。请使用 --business-direction 或在 zhuanspec/.business-direction、zhuanspec/project.md 中配置。如果是工具类/纯本地变更,请使用 --skip-specs 跳过远端同步。');
|
|
33
|
+
}
|
|
34
|
+
if (businessDirection) {
|
|
35
|
+
console.log(chalk.gray(`业务方向:${businessDirection}`));
|
|
36
|
+
await this.refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(chalk.gray('--skip-specs 模式:跳过远端模板拉取。'));
|
|
31
40
|
}
|
|
32
|
-
console.log(chalk.gray(`业务方向:${businessDirection}`));
|
|
33
|
-
await this.refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection);
|
|
34
41
|
// Get change name interactively if not provided
|
|
35
42
|
if (!changeName) {
|
|
36
43
|
const selectedChange = await this.selectChange(changesDir);
|
|
@@ -255,7 +262,12 @@ export class ArchiveCommand {
|
|
|
255
262
|
await postArchiveHook({ archived: archiveName, json: false });
|
|
256
263
|
// Push archive result (specs + archive + knowledge) to remote
|
|
257
264
|
console.log(chalk.cyan('\n━━━ 推送到远端 ━━━'));
|
|
258
|
-
|
|
265
|
+
if (businessDirection) {
|
|
266
|
+
await this.pushArchiveResultToRemote(zhuanspecDir, businessDirection);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.log(chalk.gray(' --skip-specs 模式:跳过远端推送。'));
|
|
270
|
+
}
|
|
259
271
|
// Output feedback link after archive completes
|
|
260
272
|
console.log(chalk.cyan('\n━━━ 使用反馈 ━━━'));
|
|
261
273
|
console.log(chalk.yellow(' 请填写使用反馈,帮助我们改进 ZhuanSpec 工作流:'));
|
|
@@ -519,7 +531,9 @@ export class ArchiveCommand {
|
|
|
519
531
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecDir, 'specs'));
|
|
520
532
|
console.log(chalk.gray(' → 同步 changes 目录...'));
|
|
521
533
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecDir, 'changes'));
|
|
522
|
-
console.log(chalk.
|
|
534
|
+
console.log(chalk.gray(' → 同步 knowledge 目录...'));
|
|
535
|
+
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecDir, 'knowledge'));
|
|
536
|
+
console.log(chalk.green(`✅ 已拉取 ${businessDirection} 业务模板(specs/changes/knowledge)。`));
|
|
523
537
|
}
|
|
524
538
|
finally {
|
|
525
539
|
rmSync(tempRepo, { recursive: true, force: true });
|
|
@@ -591,13 +605,8 @@ export class ArchiveCommand {
|
|
|
591
605
|
});
|
|
592
606
|
}
|
|
593
607
|
hasGitChanges(repoDir) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return output.trim().length > 0;
|
|
597
|
-
}
|
|
598
|
-
catch {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
608
|
+
const diffResult = GitRepoDetector.getGitDiff(repoDir);
|
|
609
|
+
return diffResult.changedFiles.length > 0;
|
|
601
610
|
}
|
|
602
611
|
async copyDirectoryContentsIfExists(sourceDir, targetDir) {
|
|
603
612
|
try {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* - Out of scope → block with deviation handling options
|
|
13
13
|
*/
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import { execSync } from 'child_process';
|
|
16
15
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
16
|
+
import { GitRepoDetector } from '../../utils/git-repo-detector.js';
|
|
17
17
|
import { checkPreApplyConditions } from './pre-apply.js';
|
|
18
18
|
import { recordHookTrigger } from './record-progress.js';
|
|
19
19
|
// Pitfall keywords for knowledge collection
|
|
@@ -113,7 +113,7 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
113
113
|
// Resume check: when session is resumed in apply phase, do a one-time deviation scan.
|
|
114
114
|
if (phase === 'apply' && trigger === 'resume') {
|
|
115
115
|
const estimatedFiles = await parseEstimatedChanges(proposalPath);
|
|
116
|
-
const gitDiffFiles =
|
|
116
|
+
const gitDiffFiles = GitRepoDetector.getAllChanges(cwd);
|
|
117
117
|
const outOfScope = gitDiffFiles.filter((file) => {
|
|
118
118
|
if (file.includes('zhuanspec/'))
|
|
119
119
|
return false;
|
|
@@ -292,34 +292,6 @@ ZhuanSpec Iron Law 3: Reverse Sync - When deviation is found, update proposal fi
|
|
|
292
292
|
continue: true,
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
|
-
async function parseGitChangedFiles(repoRoot) {
|
|
296
|
-
try {
|
|
297
|
-
const output = execSync('git diff --name-only', {
|
|
298
|
-
cwd: repoRoot,
|
|
299
|
-
encoding: 'utf-8',
|
|
300
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
301
|
-
});
|
|
302
|
-
return output.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
// fall through
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
const status = execSync('git status --porcelain', {
|
|
309
|
-
cwd: repoRoot,
|
|
310
|
-
encoding: 'utf-8',
|
|
311
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
312
|
-
});
|
|
313
|
-
return status
|
|
314
|
-
.split('\n')
|
|
315
|
-
.map((line) => line.replace(/^\s*[A-Z?]+\s+/, '').trim())
|
|
316
|
-
.filter(Boolean);
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
// ignore
|
|
320
|
-
}
|
|
321
|
-
return [];
|
|
322
|
-
}
|
|
323
295
|
async function parseEstimatedChanges(proposalPath) {
|
|
324
296
|
try {
|
|
325
297
|
const content = await FileSystemUtils.readFile(proposalPath);
|
|
@@ -28,6 +28,7 @@ export declare function handleDeviation(context: DeviationContext): Promise<Devi
|
|
|
28
28
|
* Complete deviation handling after user confirms
|
|
29
29
|
*
|
|
30
30
|
* @param deviationId - The deviation ID to mark as resolved
|
|
31
|
+
* @param changeId - The change ID
|
|
31
32
|
* @param specChanged - List of spec files that were changed
|
|
32
33
|
* @param codeChanged - List of code files that were changed
|
|
33
34
|
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
11
|
-
import { getBeijingTime } from './record-progress.js';
|
|
11
|
+
import { getBeijingTime, atomicWriteJson, recoverProgressJsonForWrite, } from './record-progress.js';
|
|
12
12
|
/**
|
|
13
13
|
* Handle deviation correction flow
|
|
14
14
|
*
|
|
@@ -20,24 +20,50 @@ export async function handleDeviation(context) {
|
|
|
20
20
|
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
21
21
|
const changeDir = path.join(zhuanspecDir, 'changes', context.changeId);
|
|
22
22
|
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
23
|
+
const timestamp = getBeijingTime();
|
|
23
24
|
// Record deviation in progress.json
|
|
24
25
|
const deviationId = `dev-${Date.now()}`;
|
|
25
26
|
const deviationRecord = {
|
|
26
27
|
deviationId,
|
|
27
|
-
detectedAt:
|
|
28
|
+
detectedAt: timestamp,
|
|
28
29
|
triggerType: 'user-input',
|
|
29
30
|
userInput: context.userInput.slice(0, 500),
|
|
30
31
|
specChanged: [],
|
|
31
32
|
codeChanged: [],
|
|
32
33
|
resolvedAt: undefined,
|
|
33
34
|
};
|
|
34
|
-
// Update progress.json with deviation record
|
|
35
|
+
// Update progress.json with deviation record (using recovery strategy)
|
|
35
36
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
38
|
+
if (recovered) {
|
|
39
|
+
const progressData = recovered;
|
|
40
|
+
// Fill missing fields
|
|
41
|
+
progressData.deviationRecords = progressData.deviationRecords || [];
|
|
42
|
+
progressData.deviationCount = progressData.deviationCount || 0;
|
|
43
|
+
progressData.toolCalls = progressData.toolCalls || [];
|
|
44
|
+
progressData.skillCalls = progressData.skillCalls || [];
|
|
45
|
+
progressData.hookTriggers = progressData.hookTriggers || [];
|
|
46
|
+
progressData.clarifications = progressData.clarifications || [];
|
|
47
|
+
progressData.filesModified = progressData.filesModified || [];
|
|
48
|
+
progressData.completedTasks = progressData.completedTasks || [];
|
|
49
|
+
progressData.phaseTransitions = progressData.phaseTransitions || [];
|
|
50
|
+
progressData.phaseDurations = progressData.phaseDurations || [];
|
|
51
|
+
progressData.reviewStats = progressData.reviewStats || {
|
|
52
|
+
loopCount: 0,
|
|
53
|
+
criticalFixes: 0,
|
|
54
|
+
testFixes: 0,
|
|
55
|
+
consistencyFixes: 0,
|
|
56
|
+
};
|
|
57
|
+
progressData.stats = progressData.stats || {
|
|
58
|
+
tokenUsageTotal: 0,
|
|
59
|
+
contextLoad: 0,
|
|
60
|
+
durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
|
|
61
|
+
};
|
|
62
|
+
progressData.deviationRecords.push(deviationRecord);
|
|
63
|
+
progressData.deviationCount += 1;
|
|
64
|
+
progressData.lastUpdatedAt = timestamp;
|
|
65
|
+
await atomicWriteJson(progressPath, progressData);
|
|
66
|
+
}
|
|
41
67
|
}
|
|
42
68
|
// Step 1: Modify spec file first
|
|
43
69
|
return {
|
|
@@ -49,6 +75,7 @@ export async function handleDeviation(context) {
|
|
|
49
75
|
* Complete deviation handling after user confirms
|
|
50
76
|
*
|
|
51
77
|
* @param deviationId - The deviation ID to mark as resolved
|
|
78
|
+
* @param changeId - The change ID
|
|
52
79
|
* @param specChanged - List of spec files that were changed
|
|
53
80
|
* @param codeChanged - List of code files that were changed
|
|
54
81
|
*/
|
|
@@ -57,14 +84,40 @@ export async function completeDeviation(deviationId, changeId, specChanged, code
|
|
|
57
84
|
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
58
85
|
const changeDir = path.join(zhuanspecDir, 'changes', changeId);
|
|
59
86
|
const progressPath = path.join(changeDir, 'metrics', 'progress.json');
|
|
87
|
+
const timestamp = getBeijingTime();
|
|
60
88
|
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
90
|
+
if (recovered) {
|
|
91
|
+
const progressData = recovered;
|
|
92
|
+
// Fill missing fields
|
|
93
|
+
progressData.deviationRecords = progressData.deviationRecords || [];
|
|
94
|
+
progressData.toolCalls = progressData.toolCalls || [];
|
|
95
|
+
progressData.skillCalls = progressData.skillCalls || [];
|
|
96
|
+
progressData.hookTriggers = progressData.hookTriggers || [];
|
|
97
|
+
progressData.clarifications = progressData.clarifications || [];
|
|
98
|
+
progressData.filesModified = progressData.filesModified || [];
|
|
99
|
+
progressData.completedTasks = progressData.completedTasks || [];
|
|
100
|
+
progressData.phaseTransitions = progressData.phaseTransitions || [];
|
|
101
|
+
progressData.phaseDurations = progressData.phaseDurations || [];
|
|
102
|
+
progressData.reviewStats = progressData.reviewStats || {
|
|
103
|
+
loopCount: 0,
|
|
104
|
+
criticalFixes: 0,
|
|
105
|
+
testFixes: 0,
|
|
106
|
+
consistencyFixes: 0,
|
|
107
|
+
};
|
|
108
|
+
progressData.stats = progressData.stats || {
|
|
109
|
+
tokenUsageTotal: 0,
|
|
110
|
+
contextLoad: 0,
|
|
111
|
+
durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
|
|
112
|
+
};
|
|
113
|
+
const deviation = progressData.deviationRecords.find((d) => d.deviationId === deviationId);
|
|
114
|
+
if (deviation) {
|
|
115
|
+
deviation.specChanged = specChanged;
|
|
116
|
+
deviation.codeChanged = codeChanged;
|
|
117
|
+
deviation.resolvedAt = timestamp;
|
|
118
|
+
progressData.lastUpdatedAt = timestamp;
|
|
119
|
+
await atomicWriteJson(progressPath, progressData);
|
|
120
|
+
}
|
|
68
121
|
}
|
|
69
122
|
}
|
|
70
123
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
12
|
-
import {
|
|
12
|
+
import { GitRepoDetector } from '../../utils/git-repo-detector.js';
|
|
13
|
+
import { getBeijingTime, atomicWriteJson, recoverProgressJsonForWrite, } from './record-progress.js';
|
|
13
14
|
export async function postApplyHook(options) {
|
|
14
15
|
const output = await runPostApplyHook(options.change || '');
|
|
15
16
|
if (options.json) {
|
|
@@ -26,7 +27,10 @@ async function runPostApplyHook(changeId) {
|
|
|
26
27
|
systemMessage: 'Post-apply hook skipped: no change provided',
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
// Use GitRepoDetector to find the repository root
|
|
31
|
+
const zhuanspecDir = path.join(process.cwd(), 'zhuanspec');
|
|
32
|
+
const detectedRepoRoot = GitRepoDetector.findParentGitRepo(zhuanspecDir);
|
|
33
|
+
const repoRoot = detectedRepoRoot || process.cwd();
|
|
30
34
|
const changeDir = path.join(repoRoot, 'zhuanspec', 'changes', changeId);
|
|
31
35
|
return await executePostApply(changeDir, repoRoot);
|
|
32
36
|
}
|
|
@@ -100,34 +104,15 @@ async function detectDeviation(changeDir, repoRoot) {
|
|
|
100
104
|
catch {
|
|
101
105
|
// Cannot read proposal
|
|
102
106
|
}
|
|
103
|
-
// Get actual changed files
|
|
107
|
+
// Get actual changed files using GitRepoDetector
|
|
108
|
+
const zhuanspecDir = path.join(repoRoot, 'zhuanspec');
|
|
104
109
|
let actualFiles = [];
|
|
105
110
|
try {
|
|
106
|
-
|
|
107
|
-
cwd: repoRoot,
|
|
108
|
-
encoding: 'utf-8',
|
|
109
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
110
|
-
});
|
|
111
|
-
actualFiles = output.trim().split('\n').filter(Boolean);
|
|
111
|
+
actualFiles = GitRepoDetector.getAllChanges(repoRoot);
|
|
112
112
|
}
|
|
113
113
|
catch {
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
const statusOutput = execSync('git status --porcelain', {
|
|
117
|
-
cwd: repoRoot,
|
|
118
|
-
encoding: 'utf-8',
|
|
119
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
120
|
-
});
|
|
121
|
-
actualFiles = statusOutput
|
|
122
|
-
.trim()
|
|
123
|
-
.split('\n')
|
|
124
|
-
.map(line => line.replace(/^\s*[A-Z?]+\s+/, '').trim())
|
|
125
|
-
.filter(Boolean);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
// No git info available
|
|
129
|
-
return { detected: false };
|
|
130
|
-
}
|
|
114
|
+
// No git info available
|
|
115
|
+
return { detected: false };
|
|
131
116
|
}
|
|
132
117
|
// Find files outside proposal scope (excluding zhuanspec internal files)
|
|
133
118
|
const outOfScopeFiles = [];
|
|
@@ -145,29 +130,107 @@ async function detectDeviation(changeDir, repoRoot) {
|
|
|
145
130
|
};
|
|
146
131
|
}
|
|
147
132
|
/**
|
|
148
|
-
* Update progress.json to track phase transitions
|
|
133
|
+
* Update progress.json to track phase transitions (with data preservation)
|
|
149
134
|
*/
|
|
150
135
|
async function updateProgressJson(changeDir, event) {
|
|
151
136
|
const metricsDir = path.join(changeDir, 'metrics');
|
|
152
137
|
const progressPath = path.join(metricsDir, 'progress.json');
|
|
138
|
+
const timestamp = getBeijingTime();
|
|
153
139
|
await FileSystemUtils.createDirectory(metricsDir);
|
|
154
|
-
let progress
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
let progress;
|
|
141
|
+
// Use three-layer recovery strategy to preserve all data
|
|
142
|
+
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
143
|
+
const recovered = await recoverProgressJsonForWrite(progressPath);
|
|
144
|
+
if (recovered) {
|
|
145
|
+
progress = recovered;
|
|
146
|
+
// Fill missing fields with defaults
|
|
147
|
+
progress.toolCalls = progress.toolCalls || [];
|
|
148
|
+
progress.skillCalls = progress.skillCalls || [];
|
|
149
|
+
progress.hookTriggers = progress.hookTriggers || [];
|
|
150
|
+
progress.clarifications = progress.clarifications || [];
|
|
151
|
+
progress.filesModified = progress.filesModified || [];
|
|
152
|
+
progress.completedTasks = progress.completedTasks || [];
|
|
153
|
+
progress.deviationRecords = progress.deviationRecords || [];
|
|
154
|
+
progress.phaseTransitions = progress.phaseTransitions || [];
|
|
155
|
+
progress.phaseDurations = progress.phaseDurations || [];
|
|
156
|
+
progress.reviewStats = progress.reviewStats || {
|
|
157
|
+
loopCount: 0,
|
|
158
|
+
criticalFixes: 0,
|
|
159
|
+
testFixes: 0,
|
|
160
|
+
consistencyFixes: 0,
|
|
161
|
+
};
|
|
162
|
+
progress.stats = progress.stats || {
|
|
163
|
+
tokenUsageTotal: 0,
|
|
164
|
+
contextLoad: 0,
|
|
165
|
+
durationMs: { propose: 0, apply: 0, review: 0, archive: 0 },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Recovery failed - create minimal valid structure
|
|
170
|
+
progress = {
|
|
171
|
+
changeId: path.basename(changeDir),
|
|
172
|
+
sessionId: '',
|
|
173
|
+
startedAt: timestamp,
|
|
174
|
+
lastUpdatedAt: timestamp,
|
|
175
|
+
phase: 'apply',
|
|
176
|
+
currentNode: 'apply',
|
|
177
|
+
currentTask: '',
|
|
178
|
+
completedTasks: [],
|
|
179
|
+
totalTasks: 0,
|
|
180
|
+
toolCalls: [],
|
|
181
|
+
skillCalls: [],
|
|
182
|
+
hookTriggers: [],
|
|
183
|
+
clarifications: [],
|
|
184
|
+
filesModified: [],
|
|
185
|
+
linesAdded: 0,
|
|
186
|
+
linesRemoved: 0,
|
|
187
|
+
deviationCount: 0,
|
|
188
|
+
deviationRecords: [],
|
|
189
|
+
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
190
|
+
phaseTransitions: [],
|
|
191
|
+
phaseDurations: [],
|
|
192
|
+
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
193
|
+
};
|
|
159
194
|
}
|
|
160
195
|
}
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
else {
|
|
197
|
+
// No existing file - create new structure
|
|
198
|
+
progress = {
|
|
199
|
+
changeId: path.basename(changeDir),
|
|
200
|
+
sessionId: '',
|
|
201
|
+
startedAt: timestamp,
|
|
202
|
+
lastUpdatedAt: timestamp,
|
|
203
|
+
phase: 'apply',
|
|
204
|
+
currentNode: 'apply',
|
|
205
|
+
currentTask: '',
|
|
206
|
+
completedTasks: [],
|
|
207
|
+
totalTasks: 0,
|
|
208
|
+
toolCalls: [],
|
|
209
|
+
skillCalls: [],
|
|
210
|
+
hookTriggers: [],
|
|
211
|
+
clarifications: [],
|
|
212
|
+
filesModified: [],
|
|
213
|
+
linesAdded: 0,
|
|
214
|
+
linesRemoved: 0,
|
|
215
|
+
deviationCount: 0,
|
|
216
|
+
deviationRecords: [],
|
|
217
|
+
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
218
|
+
phaseTransitions: [],
|
|
219
|
+
phaseDurations: [],
|
|
220
|
+
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Add event to events array (optional tracking)
|
|
224
|
+
if (!progress.events) {
|
|
225
|
+
progress.events = [];
|
|
163
226
|
}
|
|
164
|
-
progress.lastEvent = event;
|
|
165
|
-
progress.lastUpdatedAt = getBeijingTime();
|
|
166
|
-
progress.events = progress.events || [];
|
|
167
227
|
progress.events.push({
|
|
168
228
|
event,
|
|
169
|
-
timestamp
|
|
229
|
+
timestamp,
|
|
170
230
|
});
|
|
171
|
-
|
|
231
|
+
progress.lastEvent = event;
|
|
232
|
+
progress.lastUpdatedAt = timestamp;
|
|
233
|
+
// Use atomic write to prevent corruption
|
|
234
|
+
await atomicWriteJson(progressPath, progress);
|
|
172
235
|
}
|
|
173
236
|
//# sourceMappingURL=post-apply.js.map
|