@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
|
@@ -25,7 +25,7 @@ function generateCodeReviewPrompt(changeId) {
|
|
|
25
25
|
1. 执行 code-review-expert skill 进行代码审查
|
|
26
26
|
2. 如果发现 Critical 问题,立即修复
|
|
27
27
|
3. 修复后重新检查(最多3轮)
|
|
28
|
-
4. 输出 code-review-result.json 报告到 zhuanspec
|
|
28
|
+
4. 输出 code-review-result.json 报告到 zhuanspec/changes/${changeId}/review/
|
|
29
29
|
|
|
30
30
|
## 输出格式
|
|
31
31
|
{
|
|
@@ -50,7 +50,7 @@ function generateUnitTestPrompt(changeId) {
|
|
|
50
50
|
2. 运行单测,检查通过率和覆盖率
|
|
51
51
|
3. 如果失败或不达标,立即修复
|
|
52
52
|
4. 修复后重新测试(最多3轮)
|
|
53
|
-
5. 输出 unit-test-result.json 报告到 zhuanspec
|
|
53
|
+
5. 输出 unit-test-result.json 报告到 zhuanspec/changes/${changeId}/review/
|
|
54
54
|
|
|
55
55
|
## 输出格式
|
|
56
56
|
{
|
|
@@ -69,22 +69,69 @@ function generateSpecConsistencyPrompt(changeId) {
|
|
|
69
69
|
执行 Spec-Code 一致性检查,发现未覆盖 Scenario 时启动修复循环:
|
|
70
70
|
|
|
71
71
|
## 任务目标
|
|
72
|
-
验证 changeId=${changeId} 的代码实现与 Spec
|
|
72
|
+
验证 changeId=${changeId} 的代码实现与 Spec Scenario 语义一致。
|
|
73
|
+
⚠️ 禁止用 Requirement 标题做字符串匹配,必须逐条读取 Scenario 内容做语义比对。
|
|
73
74
|
|
|
74
75
|
## 自闭环指令
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
|
|
77
|
+
### Step 1:解析 Spec Scenarios
|
|
78
|
+
读取 zhuanspec/changes/${changeId}/specs/**/*.md,提取所有内容:
|
|
79
|
+
- 每个 \`### Requirement:\` 下的每个 \`#### Scenario:\` 标题
|
|
80
|
+
- Scenario 的 Given/When/Then 描述(业务语义)
|
|
81
|
+
- 标记每个 Scenario 的所属 Requirement
|
|
82
|
+
|
|
83
|
+
输出 Scenario 清单,格式:
|
|
84
|
+
[Requirement名] > [Scenario名]: [Given/When/Then核心语义]
|
|
85
|
+
|
|
86
|
+
### Step 2:逐 Scenario 语义比对
|
|
87
|
+
对每个 Scenario,在代码中找对应实现:
|
|
88
|
+
|
|
89
|
+
**查找顺序**:
|
|
90
|
+
1. 读取 proposal.md 的「后端改动」和「前端改动」表格,定位涉及文件
|
|
91
|
+
2. 在对应文件中找与 Scenario 语义匹配的方法/逻辑:
|
|
92
|
+
- Scenario 描述「新建客户」→ 找 createCustomer 相关方法
|
|
93
|
+
- Scenario 描述「手机号去重」→ 找手机号唯一性校验逻辑
|
|
94
|
+
- Scenario 描述「租户隔离」→ 找 tenantId 过滤条件
|
|
95
|
+
- Scenario 描述「删除企微客户拒绝」→ 找 source_type 判断逻辑
|
|
96
|
+
3. 验证找到的代码是否满足 Scenario 的 Then 条件
|
|
97
|
+
|
|
98
|
+
**判断标准**(语义覆盖,不是字符串匹配):
|
|
99
|
+
- ✅ 覆盖:代码中存在与 Scenario Given→When→Then 对应的业务逻辑
|
|
100
|
+
- ❌ 未覆盖:相关文件不存在、方法不存在、或 Then 条件未实现
|
|
101
|
+
- ⚠️ 部分覆盖:正向路径实现了,异常路径未实现
|
|
102
|
+
|
|
103
|
+
### Step 3:输出比对结果表格
|
|
104
|
+
\`\`\`
|
|
105
|
+
| Requirement | Scenario | 对应文件/方法 | 覆盖状态 | 说明 |
|
|
106
|
+
|---|---|---|---|---|
|
|
107
|
+
| 客户CRUD | 新建客户-成功 | CustomerDomainService#createCustomer | ✅ | - |
|
|
108
|
+
| 客户CRUD | 手机号重复-拒绝 | CustomerDomainService#checkPhoneDuplicate | ✅ | - |
|
|
109
|
+
| 多租户隔离 | 租户隔离查询 | CustomerEsQueryService | ❌ | 未找到tenantId过滤 |
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
### Step 4:自闭环修复
|
|
113
|
+
- 发现 ❌ 未覆盖的 Scenario → 补充对应代码实现
|
|
114
|
+
- 修复后重新执行 Step 2 验证(最多3轮)
|
|
115
|
+
- ⚠️ 仅修复真实遗漏,禁止把"文件存在但逻辑未实现"误报为已覆盖
|
|
116
|
+
|
|
117
|
+
### Step 5:输出 spec-consistency-result.json
|
|
118
|
+
路径:zhuanspec/changes/${changeId}/review/spec-consistency-result.json
|
|
80
119
|
|
|
81
120
|
## 输出格式
|
|
82
121
|
{
|
|
83
|
-
"consistencyRate": number,
|
|
122
|
+
"consistencyRate": number, // 覆盖 Scenario 数 / 总 Scenario 数
|
|
84
123
|
"totalRequirements": number,
|
|
85
124
|
"totalScenarios": number,
|
|
86
125
|
"coveredScenarios": number,
|
|
87
|
-
"uncoveredScenarios": [
|
|
126
|
+
"uncoveredScenarios": [ // 真实未覆盖,非字符串匹配失败
|
|
127
|
+
{
|
|
128
|
+
"requirement": "Requirement名",
|
|
129
|
+
"scenario": "Scenario名",
|
|
130
|
+
"reason": "具体原因:方法不存在/逻辑未实现/文件缺失",
|
|
131
|
+
"suggestedFix": "建议修复方式"
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"partialCoverage": [...], // 部分覆盖的 Scenario
|
|
88
135
|
"loopCount": number,
|
|
89
136
|
"finalStatus": "PASS" | "FAIL"
|
|
90
137
|
}
|
|
@@ -100,7 +147,7 @@ export async function orchestrateReview(options) {
|
|
|
100
147
|
const { changeId } = options;
|
|
101
148
|
// Ensure output directory exists
|
|
102
149
|
const cwd = process.cwd();
|
|
103
|
-
const outputDir = path.join(cwd, 'zhuanspec', '
|
|
150
|
+
const outputDir = path.join(cwd, 'zhuanspec', 'changes', changeId, 'review');
|
|
104
151
|
await FileSystemUtils.createDirectory(outputDir);
|
|
105
152
|
return {
|
|
106
153
|
codeReviewPrompt: generateCodeReviewPrompt(changeId),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse Hook - TDD Phase Tracking
|
|
3
|
+
*
|
|
4
|
+
* This hook tracks TDD workflow compliance for tasks annotated with @test-case:
|
|
5
|
+
* 1. Tracks file creation order (test files should precede implementation files)
|
|
6
|
+
* 2. Detects test execution results (RED → GREEN → REFACTOR phases)
|
|
7
|
+
* 3. Updates metrics/tdd-phases.json with current TDD status
|
|
8
|
+
*/
|
|
9
|
+
export type TddPhase = 'PENDING' | 'RED' | 'GREEN' | 'REFACTOR' | 'DONE';
|
|
10
|
+
export interface TddTaskStatus {
|
|
11
|
+
taskId: string;
|
|
12
|
+
testCases: string[];
|
|
13
|
+
tddPhase: TddPhase;
|
|
14
|
+
testFileCreated: boolean;
|
|
15
|
+
testInitiallyFailed: boolean;
|
|
16
|
+
testFinallyPassed: boolean;
|
|
17
|
+
implFileCreated: boolean;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TddHookResult {
|
|
21
|
+
action: 'continue' | 'warn' | 'block';
|
|
22
|
+
message?: string;
|
|
23
|
+
updatedStatus?: TddTaskStatus;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Determine whether a file path looks like a test file.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isTestFile(filePath: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Load the persisted TDD status for a given task.
|
|
31
|
+
* Returns `null` when no status has been recorded yet.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadTddStatus(changePath: string, taskId: string): Promise<TddTaskStatus | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Persist the TDD status for a task into `metrics/tdd-phases.json`.
|
|
36
|
+
* Creates the file (and parent directories) when they do not exist.
|
|
37
|
+
*/
|
|
38
|
+
export declare function saveTddStatus(changePath: string, taskId: string, status: TddTaskStatus): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Determine TDD phase transition based on the current status and latest event.
|
|
41
|
+
*
|
|
42
|
+
* Transition rules:
|
|
43
|
+
* PENDING → RED when a test file is created
|
|
44
|
+
* RED → GREEN when tests pass after initial failure is recorded
|
|
45
|
+
* GREEN → REFACTOR when an implementation file is modified / created after tests pass
|
|
46
|
+
* REFACTOR → DONE when tests still pass after refactoring
|
|
47
|
+
*/
|
|
48
|
+
export declare function detectTddPhaseTransition(currentStatus: TddTaskStatus, toolName: string, filePath: string, toolOutput?: string): TddTaskStatus;
|
|
49
|
+
/**
|
|
50
|
+
* TDD Phase Hook – called after tool use to enforce TDD discipline.
|
|
51
|
+
*
|
|
52
|
+
* @param changePath Absolute path to the current change directory
|
|
53
|
+
* @param toolName Name of the tool just used (e.g. 'Write', 'Edit', 'Bash')
|
|
54
|
+
* @param filePath File affected by the tool invocation
|
|
55
|
+
* @param taskId Current task identifier (e.g. "2.1")
|
|
56
|
+
* @param toolOutput Optional output captured from Bash executions
|
|
57
|
+
*/
|
|
58
|
+
export declare function tddPhaseHook(changePath: string, toolName: string, filePath: string, taskId: string, toolOutput?: string): Promise<TddHookResult>;
|
|
59
|
+
//# sourceMappingURL=tdd-phase-hook.d.ts.map
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse Hook - TDD Phase Tracking
|
|
3
|
+
*
|
|
4
|
+
* This hook tracks TDD workflow compliance for tasks annotated with @test-case:
|
|
5
|
+
* 1. Tracks file creation order (test files should precede implementation files)
|
|
6
|
+
* 2. Detects test execution results (RED → GREEN → REFACTOR phases)
|
|
7
|
+
* 3. Updates metrics/tdd-phases.json with current TDD status
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
11
|
+
import { atomicWriteJson } from './record-progress.js';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Test-file detection patterns
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Common test-file naming patterns across languages */
|
|
16
|
+
const TEST_FILE_PATTERNS = [
|
|
17
|
+
/Test\.java$/, // Java – JUnit / TestNG
|
|
18
|
+
/Tests?\.cs$/, // C# – xUnit / NUnit
|
|
19
|
+
/\.test\.[jt]sx?$/, // JS/TS – Jest / Vitest
|
|
20
|
+
/\.spec\.[jt]sx?$/, // JS/TS – Jasmine / Mocha / Vitest
|
|
21
|
+
/_test\.go$/, // Go
|
|
22
|
+
/_test\.py$/, // Python – pytest
|
|
23
|
+
/test_.*\.py$/, // Python – unittest
|
|
24
|
+
/Spec\.(?:scala|kt)$/, // Scala / Kotlin – ScalaTest / Kotest
|
|
25
|
+
];
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Test-result detection patterns (for Bash output)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const TEST_FAILURE_PATTERNS = [
|
|
30
|
+
/FAIL(?:ED|URE)?/i,
|
|
31
|
+
/Tests?\s+failed/i,
|
|
32
|
+
/BUILD FAILURE/i,
|
|
33
|
+
/AssertionError/i,
|
|
34
|
+
/AssertError/i,
|
|
35
|
+
/FAILURES!/,
|
|
36
|
+
/failures:\s*[1-9]/i,
|
|
37
|
+
/errors:\s*[1-9]/i,
|
|
38
|
+
/TEST.*FAIL/i,
|
|
39
|
+
];
|
|
40
|
+
const TEST_SUCCESS_PATTERNS = [
|
|
41
|
+
/PASS(?:ED)?/i,
|
|
42
|
+
/Tests?\s+passed/i,
|
|
43
|
+
/BUILD SUCCESS/i,
|
|
44
|
+
/All tests passed/i,
|
|
45
|
+
/Tests:\s+\d+ passed,\s+\d+ total/i,
|
|
46
|
+
/OK \(\d+ tests?\)/i,
|
|
47
|
+
/\d+ passing/i,
|
|
48
|
+
/failures:\s*0.*errors:\s*0/i,
|
|
49
|
+
];
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Return the path to the TDD phases JSON inside a change directory.
|
|
55
|
+
*/
|
|
56
|
+
function tddPhasesPath(changePath) {
|
|
57
|
+
return path.join(changePath, 'metrics', 'tdd-phases.json');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Determine whether a file path looks like a test file.
|
|
61
|
+
*/
|
|
62
|
+
export function isTestFile(filePath) {
|
|
63
|
+
const basename = path.basename(filePath);
|
|
64
|
+
return TEST_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load the persisted TDD status for a given task.
|
|
68
|
+
* Returns `null` when no status has been recorded yet.
|
|
69
|
+
*/
|
|
70
|
+
export async function loadTddStatus(changePath, taskId) {
|
|
71
|
+
const jsonPath = tddPhasesPath(changePath);
|
|
72
|
+
if (!(await FileSystemUtils.fileExists(jsonPath))) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const content = await FileSystemUtils.readFile(jsonPath);
|
|
77
|
+
const data = JSON.parse(content);
|
|
78
|
+
return data.tasks?.[taskId] ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Corrupted or unreadable – treat as absent
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Persist the TDD status for a task into `metrics/tdd-phases.json`.
|
|
87
|
+
* Creates the file (and parent directories) when they do not exist.
|
|
88
|
+
*/
|
|
89
|
+
export async function saveTddStatus(changePath, taskId, status) {
|
|
90
|
+
const jsonPath = tddPhasesPath(changePath);
|
|
91
|
+
let data = { tasks: {}, lastUpdated: '' };
|
|
92
|
+
if (await FileSystemUtils.fileExists(jsonPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const content = await FileSystemUtils.readFile(jsonPath);
|
|
95
|
+
data = JSON.parse(content);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Start fresh when the file is corrupted
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
data.tasks[taskId] = status;
|
|
102
|
+
data.lastUpdated = new Date().toISOString();
|
|
103
|
+
// Ensure the metrics directory exists
|
|
104
|
+
const metricsDir = path.dirname(jsonPath);
|
|
105
|
+
await FileSystemUtils.createDirectory(metricsDir);
|
|
106
|
+
await atomicWriteJson(jsonPath, data);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Detect whether a Bash tool invocation contains test-execution output
|
|
110
|
+
* and whether the tests passed or failed.
|
|
111
|
+
*
|
|
112
|
+
* Returns `'pass'`, `'fail'`, or `null` (when unable to determine).
|
|
113
|
+
*/
|
|
114
|
+
function detectTestResult(toolOutput) {
|
|
115
|
+
if (!toolOutput)
|
|
116
|
+
return null;
|
|
117
|
+
const hasFail = TEST_FAILURE_PATTERNS.some((p) => p.test(toolOutput));
|
|
118
|
+
const hasPass = TEST_SUCCESS_PATTERNS.some((p) => p.test(toolOutput));
|
|
119
|
+
// Failure takes precedence when both signals are present (e.g. "1 passed, 1 failed")
|
|
120
|
+
if (hasFail)
|
|
121
|
+
return 'fail';
|
|
122
|
+
if (hasPass)
|
|
123
|
+
return 'pass';
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Determine TDD phase transition based on the current status and latest event.
|
|
128
|
+
*
|
|
129
|
+
* Transition rules:
|
|
130
|
+
* PENDING → RED when a test file is created
|
|
131
|
+
* RED → GREEN when tests pass after initial failure is recorded
|
|
132
|
+
* GREEN → REFACTOR when an implementation file is modified / created after tests pass
|
|
133
|
+
* REFACTOR → DONE when tests still pass after refactoring
|
|
134
|
+
*/
|
|
135
|
+
export function detectTddPhaseTransition(currentStatus, toolName, filePath, toolOutput) {
|
|
136
|
+
const status = { ...currentStatus, updatedAt: new Date().toISOString() };
|
|
137
|
+
const fileIsTest = isTestFile(filePath);
|
|
138
|
+
// Track file creation
|
|
139
|
+
if (fileIsTest) {
|
|
140
|
+
status.testFileCreated = true;
|
|
141
|
+
}
|
|
142
|
+
else if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
143
|
+
status.implFileCreated = true;
|
|
144
|
+
}
|
|
145
|
+
// Detect test execution results (only relevant when toolName is Bash)
|
|
146
|
+
if (toolName === 'Bash') {
|
|
147
|
+
const result = detectTestResult(toolOutput);
|
|
148
|
+
switch (status.tddPhase) {
|
|
149
|
+
case 'PENDING':
|
|
150
|
+
// If tests are already running and they fail → move to RED
|
|
151
|
+
if (result === 'fail') {
|
|
152
|
+
status.testInitiallyFailed = true;
|
|
153
|
+
status.tddPhase = 'RED';
|
|
154
|
+
}
|
|
155
|
+
// If a test file was just created, also transition to RED
|
|
156
|
+
if (status.testFileCreated && status.tddPhase === 'PENDING') {
|
|
157
|
+
status.tddPhase = 'RED';
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case 'RED':
|
|
161
|
+
if (result === 'fail') {
|
|
162
|
+
status.testInitiallyFailed = true;
|
|
163
|
+
}
|
|
164
|
+
if (result === 'pass' && status.testInitiallyFailed) {
|
|
165
|
+
status.testFinallyPassed = true;
|
|
166
|
+
status.tddPhase = 'GREEN';
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case 'GREEN':
|
|
170
|
+
// After GREEN, any passing test run during refactor moves to REFACTOR
|
|
171
|
+
if (result === 'pass') {
|
|
172
|
+
status.tddPhase = 'REFACTOR';
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case 'REFACTOR':
|
|
176
|
+
// Stays in REFACTOR; transition to DONE is handled externally
|
|
177
|
+
// when the task is marked complete.
|
|
178
|
+
if (result === 'pass') {
|
|
179
|
+
status.tddPhase = 'DONE';
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Also transition PENDING → RED when a test file is created (non-Bash tool)
|
|
187
|
+
if (status.tddPhase === 'PENDING' && status.testFileCreated) {
|
|
188
|
+
status.tddPhase = 'RED';
|
|
189
|
+
}
|
|
190
|
+
return status;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check whether the given task line from tasks.md contains a @test-case annotation.
|
|
194
|
+
* Returns the list of referenced TC identifiers, or an empty array.
|
|
195
|
+
*/
|
|
196
|
+
function parseTestCaseAnnotations(taskLine) {
|
|
197
|
+
const match = taskLine.match(/@test-case[:\s]+([^\n]+)/i);
|
|
198
|
+
if (!match)
|
|
199
|
+
return [];
|
|
200
|
+
return match[1]
|
|
201
|
+
.split(/[,\s]+/)
|
|
202
|
+
.map((tc) => tc.trim())
|
|
203
|
+
.filter(Boolean);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Read tasks.md and locate the line that corresponds to the given taskId.
|
|
207
|
+
* Returns `null` when the task is not found.
|
|
208
|
+
*/
|
|
209
|
+
async function findTaskLine(changePath, taskId) {
|
|
210
|
+
const tasksPath = path.join(changePath, 'tasks.md');
|
|
211
|
+
if (!(await FileSystemUtils.fileExists(tasksPath))) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const content = await FileSystemUtils.readFile(tasksPath);
|
|
215
|
+
const lines = content.split('\n');
|
|
216
|
+
// Normalise taskId for comparison (e.g. "2.1" should match "2.1")
|
|
217
|
+
const normalised = taskId.trim();
|
|
218
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
219
|
+
const line = lines[idx];
|
|
220
|
+
// Match checkbox task lines: "- [ ] 2.1 ..." or "- [x] 2.1 ..."
|
|
221
|
+
const taskMatch = line.match(/^-\s+\[[ x]\]\s+(\S+)/);
|
|
222
|
+
if (taskMatch && taskMatch[1] === normalised) {
|
|
223
|
+
let fullBlock = line;
|
|
224
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
225
|
+
if (/^\s{2,}/.test(lines[i]) && !/^-\s+\[/.test(lines[i].trim())) {
|
|
226
|
+
fullBlock += '\n' + lines[i];
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fullBlock;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Main hook entry point
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* TDD Phase Hook – called after tool use to enforce TDD discipline.
|
|
242
|
+
*
|
|
243
|
+
* @param changePath Absolute path to the current change directory
|
|
244
|
+
* @param toolName Name of the tool just used (e.g. 'Write', 'Edit', 'Bash')
|
|
245
|
+
* @param filePath File affected by the tool invocation
|
|
246
|
+
* @param taskId Current task identifier (e.g. "2.1")
|
|
247
|
+
* @param toolOutput Optional output captured from Bash executions
|
|
248
|
+
*/
|
|
249
|
+
export async function tddPhaseHook(changePath, toolName, filePath, taskId, toolOutput) {
|
|
250
|
+
// 1. Determine whether this task is a TDD task (@test-case annotated)
|
|
251
|
+
const taskLine = await findTaskLine(changePath, taskId);
|
|
252
|
+
if (!taskLine) {
|
|
253
|
+
return { action: 'continue' };
|
|
254
|
+
}
|
|
255
|
+
const testCases = parseTestCaseAnnotations(taskLine);
|
|
256
|
+
if (testCases.length === 0) {
|
|
257
|
+
// Not a TDD task – nothing to enforce
|
|
258
|
+
return { action: 'continue' };
|
|
259
|
+
}
|
|
260
|
+
// 2. Load or initialise TDD status
|
|
261
|
+
let status = await loadTddStatus(changePath, taskId);
|
|
262
|
+
if (!status) {
|
|
263
|
+
status = {
|
|
264
|
+
taskId,
|
|
265
|
+
testCases,
|
|
266
|
+
tddPhase: 'PENDING',
|
|
267
|
+
testFileCreated: false,
|
|
268
|
+
testInitiallyFailed: false,
|
|
269
|
+
testFinallyPassed: false,
|
|
270
|
+
implFileCreated: false,
|
|
271
|
+
updatedAt: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// 3. Check file creation order: test file should be created before impl file
|
|
275
|
+
if (!isTestFile(filePath) && !status.testFileCreated) {
|
|
276
|
+
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
277
|
+
// Implementation file created/modified before any test file exists
|
|
278
|
+
status.implFileCreated = true;
|
|
279
|
+
status.updatedAt = new Date().toISOString();
|
|
280
|
+
await saveTddStatus(changePath, taskId, status);
|
|
281
|
+
return {
|
|
282
|
+
action: 'warn',
|
|
283
|
+
message: `TDD violation: Implementation file "${path.basename(filePath)}" ` +
|
|
284
|
+
`created before test file. In TDD, write tests first (RED phase).`,
|
|
285
|
+
updatedStatus: status,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// 4. Detect phase transitions & test results
|
|
290
|
+
const previousPhase = status.tddPhase;
|
|
291
|
+
status = detectTddPhaseTransition(status, toolName, filePath, toolOutput);
|
|
292
|
+
// 5. Enforce GREEN-phase constraint: tests must pass
|
|
293
|
+
if (previousPhase === 'GREEN' && toolName === 'Bash') {
|
|
294
|
+
const result = detectTestResult(toolOutput);
|
|
295
|
+
if (result === 'fail') {
|
|
296
|
+
await saveTddStatus(changePath, taskId, status);
|
|
297
|
+
return {
|
|
298
|
+
action: 'block',
|
|
299
|
+
message: 'TDD violation: Tests must pass in GREEN phase. ' +
|
|
300
|
+
'Fix the implementation until all tests pass before proceeding.',
|
|
301
|
+
updatedStatus: status,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 6. Persist updated status
|
|
306
|
+
await saveTddStatus(changePath, taskId, status);
|
|
307
|
+
// 7. Return result
|
|
308
|
+
return {
|
|
309
|
+
action: 'continue',
|
|
310
|
+
updatedStatus: status,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=tdd-phase-hook.js.map
|
package/dist/core/init.d.ts
CHANGED
package/dist/core/init.js
CHANGED
|
@@ -353,6 +353,8 @@ export class InitCommand {
|
|
|
353
353
|
symbol: PALETTE.white('▌'),
|
|
354
354
|
text: PALETTE.white('AI 工具已配置'),
|
|
355
355
|
});
|
|
356
|
+
// Step 4: Ensure claude-hud is installed (for version display in statusLine)
|
|
357
|
+
await this.ensureClaudeHud();
|
|
356
358
|
// Success message
|
|
357
359
|
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, toolConfigResult.rootStubStatus, toolConfigResult.claudeHookSummary);
|
|
358
360
|
}
|
|
@@ -1258,6 +1260,17 @@ export class InitCommand {
|
|
|
1258
1260
|
],
|
|
1259
1261
|
},
|
|
1260
1262
|
],
|
|
1263
|
+
UserPromptSubmit: [
|
|
1264
|
+
{
|
|
1265
|
+
hooks: [
|
|
1266
|
+
{
|
|
1267
|
+
type: 'command',
|
|
1268
|
+
command: 'zhuanspec-hook deviation-check --trigger post-prompt',
|
|
1269
|
+
statusMessage: 'Checking prompt vs proposal consistency...',
|
|
1270
|
+
},
|
|
1271
|
+
],
|
|
1272
|
+
},
|
|
1273
|
+
],
|
|
1261
1274
|
PreToolUse: [
|
|
1262
1275
|
{
|
|
1263
1276
|
matcher: 'Write|Edit',
|
|
@@ -1280,10 +1293,15 @@ export class InitCommand {
|
|
|
1280
1293
|
statusMessage: '📝 Recording progress...',
|
|
1281
1294
|
async: false,
|
|
1282
1295
|
},
|
|
1296
|
+
],
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
matcher: 'Write|Edit',
|
|
1300
|
+
hooks: [
|
|
1283
1301
|
{
|
|
1284
1302
|
type: 'command',
|
|
1285
|
-
command: 'zhuanspec-hook collect-knowledge',
|
|
1286
|
-
statusMessage: '
|
|
1303
|
+
command: 'zhuanspec-hook collect-knowledge --json',
|
|
1304
|
+
statusMessage: '💡 Checking for knowledge to capture...',
|
|
1287
1305
|
async: false,
|
|
1288
1306
|
},
|
|
1289
1307
|
],
|
|
@@ -1526,6 +1544,31 @@ export class InitCommand {
|
|
|
1526
1544
|
console.log(PALETTE.white('Welcome to ZhuanSpec!'));
|
|
1527
1545
|
console.log();
|
|
1528
1546
|
}
|
|
1547
|
+
async ensureClaudeHud() {
|
|
1548
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || '~', '.claude');
|
|
1549
|
+
const hudConfigPath = path.join(configDir, 'plugins', 'claude-hud', 'config.json');
|
|
1550
|
+
if (existsSync(hudConfigPath)) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const spinner = this.startSpinner('正在安装 claude-hud(用于版本信息展示)...');
|
|
1554
|
+
try {
|
|
1555
|
+
execSync('claude plugin install claude-hud', {
|
|
1556
|
+
encoding: 'utf-8',
|
|
1557
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1558
|
+
timeout: 30000,
|
|
1559
|
+
});
|
|
1560
|
+
spinner.stopAndPersist({
|
|
1561
|
+
symbol: PALETTE.white('▌'),
|
|
1562
|
+
text: PALETTE.white('claude-hud 安装完成'),
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
spinner.stopAndPersist({
|
|
1567
|
+
symbol: PALETTE.midGray('▌'),
|
|
1568
|
+
text: PALETTE.midGray('claude-hud 安装失败,可手动执行: claude mcp install claude-hud'),
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1529
1572
|
startSpinner(text) {
|
|
1530
1573
|
return ora({
|
|
1531
1574
|
text,
|