ai-unit-test-generator 1.4.6 → 2.0.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/CHANGELOG.md +143 -0
- package/README.md +43 -18
- package/bin/cli.js +100 -211
- package/lib/ai/analyzer-prompt.mjs +120 -0
- package/lib/ai/config-writer.mjs +99 -0
- package/lib/ai/context-builder.mjs +52 -0
- package/lib/ai/index.mjs +11 -0
- package/lib/ai/reviewer.mjs +283 -0
- package/lib/ai/sampler.mjs +97 -0
- package/lib/ai/validator.mjs +101 -0
- package/lib/core/scanner.mjs +112 -2
- package/lib/core/scorer.mjs +184 -4
- package/lib/utils/config-manager.mjs +110 -0
- package/lib/utils/index.mjs +2 -0
- package/lib/utils/scan-manager.mjs +121 -0
- package/lib/workflows/analyze.mjs +174 -0
- package/lib/workflows/generate.mjs +98 -0
- package/lib/workflows/index.mjs +7 -0
- package/lib/workflows/init.mjs +45 -0
- package/lib/workflows/scan.mjs +144 -0
- package/package.json +1 -1
- package/templates/default.config.jsonc +58 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
/**
|
2
|
+
* Analyze 工作流:AI 分析代码库并生成配置建议
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { spawn } from 'node:child_process'
|
6
|
+
import { writeFileSync } from 'node:fs'
|
7
|
+
import { detectConfig } from '../utils/config-manager.mjs'
|
8
|
+
import { sampleCodeFiles, analyzeProjectStructure } from '../ai/sampler.mjs'
|
9
|
+
import { buildProjectContext } from '../ai/context-builder.mjs'
|
10
|
+
import { buildAnalysisPrompt } from '../ai/analyzer-prompt.mjs'
|
11
|
+
import { validateAndSanitize } from '../ai/validator.mjs'
|
12
|
+
import { interactiveReview } from '../ai/reviewer.mjs'
|
13
|
+
import { applyAISuggestions } from '../ai/config-writer.mjs'
|
14
|
+
|
15
|
+
/**
|
16
|
+
* AI 分析工作流
|
17
|
+
*/
|
18
|
+
export async function analyze(options) {
|
19
|
+
const { config, output } = options
|
20
|
+
|
21
|
+
// 1. 检查配置是否存在
|
22
|
+
console.log('🔍 Step 1: Checking configuration...')
|
23
|
+
const configPath = detectConfig(config)
|
24
|
+
|
25
|
+
if (!configPath) {
|
26
|
+
console.error('❌ Config not found. Run `ai-test init` first.')
|
27
|
+
process.exit(1)
|
28
|
+
}
|
29
|
+
|
30
|
+
console.log(` Using config: ${configPath}\n`)
|
31
|
+
|
32
|
+
// 2. 分析项目结构
|
33
|
+
console.log('📊 Step 2: Analyzing project structure...')
|
34
|
+
const stats = await analyzeProjectStructure()
|
35
|
+
console.log(` Total files: ${stats.totalFiles}`)
|
36
|
+
console.log(` Total lines: ${stats.totalLines}`)
|
37
|
+
console.log(` Avg lines/file: ${stats.avgLinesPerFile}\n`)
|
38
|
+
|
39
|
+
// 3. 智能采样代码
|
40
|
+
console.log('🎯 Step 3: Sampling representative code...')
|
41
|
+
const samples = await sampleCodeFiles()
|
42
|
+
console.log(` Selected ${samples.length} files across layers\n`)
|
43
|
+
|
44
|
+
// 4. 构建项目上下文
|
45
|
+
console.log('📦 Step 4: Reading project context...')
|
46
|
+
const projectCtx = await buildProjectContext()
|
47
|
+
console.log(` Framework: ${projectCtx.framework}`)
|
48
|
+
console.log(` Critical deps: ${projectCtx.criticalDeps.length > 0 ? projectCtx.criticalDeps.join(', ') : 'None detected'}\n`)
|
49
|
+
|
50
|
+
// 5. 构建 AI Prompt
|
51
|
+
console.log('✍️ Step 5: Building AI analysis prompt...')
|
52
|
+
const prompt = buildAnalysisPrompt(samples, stats, projectCtx)
|
53
|
+
|
54
|
+
// 保存 prompt 到临时文件
|
55
|
+
const promptPath = 'prompt_analyze.txt'
|
56
|
+
writeFileSync(promptPath, prompt, 'utf-8')
|
57
|
+
console.log(` Prompt saved to: ${promptPath}\n`)
|
58
|
+
|
59
|
+
// 6. 调用 Cursor Agent
|
60
|
+
console.log('🤖 Step 6: Calling Cursor Agent...')
|
61
|
+
console.log(' Cursor will analyze the full codebase using its index...')
|
62
|
+
console.log(' This may take 1-2 minutes...\n')
|
63
|
+
|
64
|
+
const responseText = await callCursorAgent(promptPath)
|
65
|
+
|
66
|
+
if (!responseText) {
|
67
|
+
console.error('❌ AI analysis failed or returned empty response')
|
68
|
+
process.exit(1)
|
69
|
+
}
|
70
|
+
|
71
|
+
// 7. 解析并验证响应
|
72
|
+
console.log('✅ Step 7: Validating AI suggestions...')
|
73
|
+
|
74
|
+
let parsed
|
75
|
+
try {
|
76
|
+
// 尝试提取 JSON(AI 可能返回 markdown 包装)
|
77
|
+
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/) ||
|
78
|
+
responseText.match(/```\s*([\s\S]*?)\s*```/)
|
79
|
+
|
80
|
+
const jsonText = jsonMatch ? jsonMatch[1] : responseText
|
81
|
+
parsed = JSON.parse(jsonText)
|
82
|
+
} catch (err) {
|
83
|
+
console.error('❌ Failed to parse AI response as JSON')
|
84
|
+
console.error(' Response preview:', responseText.slice(0, 500))
|
85
|
+
process.exit(1)
|
86
|
+
}
|
87
|
+
|
88
|
+
const validated = validateAndSanitize(parsed)
|
89
|
+
|
90
|
+
const totalSuggestions = Object.values(validated).reduce((sum, arr) => sum + arr.length, 0)
|
91
|
+
console.log(` Validated ${totalSuggestions} suggestions\n`)
|
92
|
+
|
93
|
+
if (totalSuggestions === 0) {
|
94
|
+
console.log('⚠️ No valid suggestions from AI. Please check the response.')
|
95
|
+
process.exit(0)
|
96
|
+
}
|
97
|
+
|
98
|
+
// 8. 交互式审核
|
99
|
+
console.log('📝 Step 8: Interactive review...\n')
|
100
|
+
const approved = await interactiveReview(validated)
|
101
|
+
|
102
|
+
if (approved === null) {
|
103
|
+
console.log('\n❌ No changes saved.')
|
104
|
+
process.exit(0)
|
105
|
+
}
|
106
|
+
|
107
|
+
// 9. 写入配置
|
108
|
+
console.log('\n💾 Step 9: Updating configuration...')
|
109
|
+
|
110
|
+
try {
|
111
|
+
await applyAISuggestions(configPath, approved)
|
112
|
+
console.log('✅ Config updated!')
|
113
|
+
console.log('\n💡 Next: Run `ai-test scan` to recalculate scores with AI enhancements.')
|
114
|
+
} catch (err) {
|
115
|
+
console.error(`❌ Failed to update config: ${err.message}`)
|
116
|
+
process.exit(1)
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* 调用 Cursor Agent
|
122
|
+
*/
|
123
|
+
async function callCursorAgent(promptPath) {
|
124
|
+
return new Promise((resolve, reject) => {
|
125
|
+
const { readFileSync } = require('node:fs')
|
126
|
+
|
127
|
+
// 读取 prompt
|
128
|
+
let prompt
|
129
|
+
try {
|
130
|
+
prompt = readFileSync(promptPath, 'utf-8')
|
131
|
+
} catch (err) {
|
132
|
+
reject(new Error(`Failed to read prompt file: ${err.message}`))
|
133
|
+
return
|
134
|
+
}
|
135
|
+
|
136
|
+
// 调用 cursor-agent chat(通过 stdin 传递 prompt)
|
137
|
+
const child = spawn('cursor-agent', ['chat'], {
|
138
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
139
|
+
shell: true,
|
140
|
+
cwd: process.cwd()
|
141
|
+
})
|
142
|
+
|
143
|
+
const chunks = []
|
144
|
+
child.stdout.on('data', (d) => chunks.push(Buffer.from(d)))
|
145
|
+
|
146
|
+
// 写入 prompt 到 stdin
|
147
|
+
child.stdin.write(prompt)
|
148
|
+
child.stdin.end()
|
149
|
+
|
150
|
+
// 超时处理(10 分钟)
|
151
|
+
const timeout = setTimeout(() => {
|
152
|
+
child.kill('SIGKILL')
|
153
|
+
reject(new Error('cursor-agent timeout after 600s'))
|
154
|
+
}, 600000)
|
155
|
+
|
156
|
+
child.on('close', (code) => {
|
157
|
+
clearTimeout(timeout)
|
158
|
+
|
159
|
+
if (code !== 0) {
|
160
|
+
reject(new Error(`cursor-agent exited with code ${code}`))
|
161
|
+
return
|
162
|
+
}
|
163
|
+
|
164
|
+
const response = Buffer.concat(chunks).toString('utf-8')
|
165
|
+
resolve(response)
|
166
|
+
})
|
167
|
+
|
168
|
+
child.on('error', (err) => {
|
169
|
+
clearTimeout(timeout)
|
170
|
+
reject(new Error(`Failed to spawn cursor-agent: ${err.message}`))
|
171
|
+
})
|
172
|
+
})
|
173
|
+
}
|
174
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
/**
|
2
|
+
* Generate 工作流:生成单元测试
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { spawn } from 'node:child_process'
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
7
|
+
import { join, dirname } from 'node:path'
|
8
|
+
import { fileURLToPath } from 'node:url'
|
9
|
+
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
11
|
+
const __dirname = dirname(__filename)
|
12
|
+
const PKG_ROOT = join(__dirname, '../..')
|
13
|
+
|
14
|
+
/**
|
15
|
+
* 生成单批测试
|
16
|
+
*/
|
17
|
+
async function generateBatch(priority, count, skip, report) {
|
18
|
+
const batchScript = join(PKG_ROOT, 'lib/workflows/batch.mjs')
|
19
|
+
|
20
|
+
return new Promise((resolve, reject) => {
|
21
|
+
const child = spawn('node', [batchScript, priority, String(count), String(skip), report], {
|
22
|
+
stdio: 'inherit',
|
23
|
+
cwd: process.cwd()
|
24
|
+
})
|
25
|
+
|
26
|
+
child.on('close', (code) => {
|
27
|
+
if (code === 0) {
|
28
|
+
resolve({ generated: count, passed: count })
|
29
|
+
} else {
|
30
|
+
reject(new Error(`Batch generation failed with code ${code}`))
|
31
|
+
}
|
32
|
+
})
|
33
|
+
|
34
|
+
child.on('error', reject)
|
35
|
+
})
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Generate 工作流
|
40
|
+
*/
|
41
|
+
export async function generate(options) {
|
42
|
+
const { count, priority, all, report } = options
|
43
|
+
|
44
|
+
// 1. 检查报告是否存在
|
45
|
+
if (!existsSync(report)) {
|
46
|
+
console.error(`❌ Report not found: ${report}`)
|
47
|
+
console.log(` Run: ai-test scan`)
|
48
|
+
process.exit(1)
|
49
|
+
}
|
50
|
+
|
51
|
+
if (all) {
|
52
|
+
// 2. 持续生成直到所有 TODO 完成
|
53
|
+
console.log(`🚀 Generating all ${priority} TODO functions...\n`)
|
54
|
+
|
55
|
+
let batchNum = 1
|
56
|
+
let totalGenerated = 0
|
57
|
+
let totalPassed = 0
|
58
|
+
|
59
|
+
while (true) {
|
60
|
+
// 检查还有多少 TODO
|
61
|
+
const content = readFileSync(report, 'utf-8')
|
62
|
+
const lines = content.split('\n')
|
63
|
+
const todoLines = lines.filter(line =>
|
64
|
+
line.includes('| TODO |') && line.includes(`| ${priority} |`)
|
65
|
+
)
|
66
|
+
|
67
|
+
if (todoLines.length === 0) {
|
68
|
+
console.log(`\n✅ All ${priority} functions completed!`)
|
69
|
+
console.log(` Total generated: ${totalGenerated}`)
|
70
|
+
console.log(` Total passed: ${totalPassed}`)
|
71
|
+
break
|
72
|
+
}
|
73
|
+
|
74
|
+
console.log(`\n━━━━ Batch ${batchNum} (${todoLines.length} TODO remaining) ━━━━`)
|
75
|
+
|
76
|
+
try {
|
77
|
+
const result = await generateBatch(priority, Math.min(count, todoLines.length), 0, report)
|
78
|
+
totalGenerated += result.generated
|
79
|
+
totalPassed += result.passed
|
80
|
+
batchNum++
|
81
|
+
} catch (err) {
|
82
|
+
console.error(`❌ Batch ${batchNum} failed:`, err.message)
|
83
|
+
break
|
84
|
+
}
|
85
|
+
}
|
86
|
+
} else {
|
87
|
+
// 3. 生成指定数量
|
88
|
+
console.log(`🚀 Generating ${count} ${priority} functions...\n`)
|
89
|
+
|
90
|
+
try {
|
91
|
+
await generateBatch(priority, count, 0, report)
|
92
|
+
} catch (err) {
|
93
|
+
console.error('❌ Generation failed:', err.message)
|
94
|
+
process.exit(1)
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
package/lib/workflows/index.mjs
CHANGED
@@ -5,6 +5,13 @@
|
|
5
5
|
* 组合 Core、AI 和 Testing 模块实现端到端工作流
|
6
6
|
*/
|
7
7
|
|
8
|
+
// 主要工作流
|
9
|
+
export * from './init.mjs'
|
10
|
+
export * from './analyze.mjs'
|
11
|
+
export * from './scan.mjs'
|
12
|
+
export * from './generate.mjs'
|
13
|
+
|
14
|
+
// 批处理工作流(内部使用)
|
8
15
|
export { main as runBatch } from './batch.mjs'
|
9
16
|
export { main as runAll } from './all.mjs'
|
10
17
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
/**
|
2
|
+
* Init 工作流:初始化配置文件
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { existsSync, copyFileSync } from 'node:fs'
|
6
|
+
import { join, dirname } from 'node:path'
|
7
|
+
import { fileURLToPath } from 'node:url'
|
8
|
+
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
10
|
+
const __dirname = dirname(__filename)
|
11
|
+
const PKG_ROOT = join(__dirname, '../..')
|
12
|
+
|
13
|
+
/**
|
14
|
+
* 初始化配置文件
|
15
|
+
*/
|
16
|
+
export async function init(options) {
|
17
|
+
const { config } = options
|
18
|
+
const configPath = config || 'ai-test.config.jsonc'
|
19
|
+
|
20
|
+
// 检查配置文件是否已存在
|
21
|
+
if (existsSync(configPath)) {
|
22
|
+
console.log(`⚠️ Config already exists: ${configPath}`)
|
23
|
+
console.log(' Delete it first or use a different path with -c option')
|
24
|
+
process.exit(1)
|
25
|
+
}
|
26
|
+
|
27
|
+
// 复制模板
|
28
|
+
console.log('⚙️ Creating default config...')
|
29
|
+
const templatePath = join(PKG_ROOT, 'templates', 'default.config.jsonc')
|
30
|
+
|
31
|
+
try {
|
32
|
+
copyFileSync(templatePath, configPath)
|
33
|
+
console.log(`✅ Config created: ${configPath}`)
|
34
|
+
|
35
|
+
// 显示下一步建议
|
36
|
+
console.log('\n💡 Next steps:')
|
37
|
+
console.log(' 1. Review and customize the config (optional)')
|
38
|
+
console.log(' 2. Run `ai-test analyze` to let AI analyze your codebase')
|
39
|
+
console.log(' 3. Or run `ai-test scan` to start scoring immediately')
|
40
|
+
} catch (err) {
|
41
|
+
console.error(`❌ Failed to create config: ${err.message}`)
|
42
|
+
process.exit(1)
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
@@ -0,0 +1,144 @@
|
|
1
|
+
/**
|
2
|
+
* Scan 工作流:扫描代码 + 打分 + 生成报告
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { spawn } from 'node:child_process'
|
6
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
7
|
+
import { join, dirname } from 'node:path'
|
8
|
+
import { fileURLToPath } from 'node:url'
|
9
|
+
import { detectConfig, readConfig } from '../utils/config-manager.mjs'
|
10
|
+
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
12
|
+
const __dirname = dirname(__filename)
|
13
|
+
const PKG_ROOT = join(__dirname, '../..')
|
14
|
+
|
15
|
+
/**
|
16
|
+
* 移除 JSON 注释
|
17
|
+
*/
|
18
|
+
function stripJsonComments(str) {
|
19
|
+
return String(str)
|
20
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
21
|
+
.replace(/(^|\s)\/\/.*$/gm, '')
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* 运行脚本
|
26
|
+
*/
|
27
|
+
function runScript(scriptPath, args) {
|
28
|
+
return new Promise((resolve, reject) => {
|
29
|
+
const fullPath = join(PKG_ROOT, 'lib', scriptPath)
|
30
|
+
const child = spawn('node', [fullPath, ...args], {
|
31
|
+
stdio: 'inherit',
|
32
|
+
cwd: process.cwd()
|
33
|
+
})
|
34
|
+
|
35
|
+
child.on('close', (code) => {
|
36
|
+
if (code === 0) {
|
37
|
+
resolve()
|
38
|
+
} else {
|
39
|
+
reject(new Error(`Script ${scriptPath} exited with code ${code}`))
|
40
|
+
}
|
41
|
+
})
|
42
|
+
|
43
|
+
child.on('error', reject)
|
44
|
+
})
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Scan 工作流
|
49
|
+
*/
|
50
|
+
export async function scan(options) {
|
51
|
+
let { config, output, skipGit } = options
|
52
|
+
|
53
|
+
// 1. 检查配置
|
54
|
+
console.log('🔍 Step 1: Checking configuration...')
|
55
|
+
const configPath = detectConfig(config)
|
56
|
+
|
57
|
+
if (!configPath) {
|
58
|
+
console.error('❌ Config not found. Run `ai-test init` first.')
|
59
|
+
process.exit(1)
|
60
|
+
}
|
61
|
+
|
62
|
+
config = configPath
|
63
|
+
console.log(` Using config: ${config}\n`)
|
64
|
+
|
65
|
+
// 2. 创建输出目录
|
66
|
+
if (!existsSync(output)) {
|
67
|
+
mkdirSync(output, { recursive: true })
|
68
|
+
}
|
69
|
+
|
70
|
+
// 3. 可选:运行覆盖率
|
71
|
+
try {
|
72
|
+
const cfgText = existsSync(config) ? readFileSync(config, 'utf-8') : '{}'
|
73
|
+
const cfg = JSON.parse(stripJsonComments(cfgText))
|
74
|
+
const covCfg = cfg?.coverage || { runBeforeScan: false }
|
75
|
+
|
76
|
+
if (covCfg.runBeforeScan) {
|
77
|
+
console.log('🧪 Running coverage before scan...')
|
78
|
+
await new Promise((resolve) => {
|
79
|
+
const cmd = covCfg.command || 'npx jest --coverage --silent'
|
80
|
+
const child = spawn(cmd, { stdio: 'inherit', shell: true, cwd: process.cwd() })
|
81
|
+
child.on('close', () => resolve())
|
82
|
+
child.on('error', () => resolve())
|
83
|
+
})
|
84
|
+
console.log('✅ Coverage completed.\n')
|
85
|
+
}
|
86
|
+
} catch (err) {
|
87
|
+
console.warn('⚠️ Coverage step failed. Continuing scan.')
|
88
|
+
}
|
89
|
+
|
90
|
+
console.log('🚀 Starting code scan...\n')
|
91
|
+
|
92
|
+
try {
|
93
|
+
// 4. 扫描 AST + 复杂度
|
94
|
+
console.log('📋 Step 2: Scanning targets...')
|
95
|
+
await runScript('core/scanner.mjs', [
|
96
|
+
'--config', config,
|
97
|
+
'--out', join(output, 'targets.json')
|
98
|
+
])
|
99
|
+
|
100
|
+
// 5. Git 信号(可选)
|
101
|
+
if (!skipGit) {
|
102
|
+
console.log('\n📊 Step 3: Analyzing Git history...')
|
103
|
+
await runScript('core/git-analyzer.mjs', [
|
104
|
+
'--targets', join(output, 'targets.json'),
|
105
|
+
'--out', join(output, 'git_signals.json')
|
106
|
+
])
|
107
|
+
}
|
108
|
+
|
109
|
+
// 6. 打分
|
110
|
+
console.log('\n🎯 Step 4: Scoring targets...')
|
111
|
+
const scoreArgs = [
|
112
|
+
'--targets', join(output, 'targets.json'),
|
113
|
+
'--config', config,
|
114
|
+
'--out-md', join(output, 'ut_scores.md'),
|
115
|
+
'--out-csv', join(output, 'ut_scores.csv')
|
116
|
+
]
|
117
|
+
|
118
|
+
if (!skipGit && existsSync(join(output, 'git_signals.json'))) {
|
119
|
+
scoreArgs.push('--git', join(output, 'git_signals.json'))
|
120
|
+
}
|
121
|
+
|
122
|
+
await runScript('core/scorer.mjs', scoreArgs)
|
123
|
+
|
124
|
+
// 7. 统计结果
|
125
|
+
const reportPath = join(output, 'ut_scores.md')
|
126
|
+
if (existsSync(reportPath)) {
|
127
|
+
const content = readFileSync(reportPath, 'utf-8')
|
128
|
+
const todoCount = (content.match(/\| TODO \|/g) || []).length
|
129
|
+
const doneCount = (content.match(/\| DONE \|/g) || []).length
|
130
|
+
|
131
|
+
console.log('\n✅ Scan completed!')
|
132
|
+
console.log(`\n📊 Status:`)
|
133
|
+
console.log(` TODO: ${todoCount}`)
|
134
|
+
console.log(` DONE: ${doneCount}`)
|
135
|
+
console.log(` Total: ${todoCount + doneCount}`)
|
136
|
+
console.log(`\n📄 Report: ${reportPath}`)
|
137
|
+
console.log(`\n💡 Next: ai-test generate`)
|
138
|
+
}
|
139
|
+
} catch (err) {
|
140
|
+
console.error('❌ Scan failed:', err.message)
|
141
|
+
process.exit(1)
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
package/package.json
CHANGED
@@ -310,5 +310,63 @@
|
|
310
310
|
"CC": 6, // 复杂度默认值
|
311
311
|
"ERLikelihood": 3, // 错误可能性默认值
|
312
312
|
"Testability": 6 // 可测试性默认值
|
313
|
+
},
|
314
|
+
|
315
|
+
// ========== AI 增强配置(AI 唯一可写区域)==========
|
316
|
+
|
317
|
+
"aiEnhancement": {
|
318
|
+
"enabled": true, // 是否启用 AI 增强
|
319
|
+
"analyzed": false, // 是否已完成 AI 分析
|
320
|
+
"analyzedAt": null, // 分析时间戳
|
321
|
+
|
322
|
+
// ✅ 业务实体关键词(用于 AST 扫描识别业务对象)
|
323
|
+
"entityKeywords": [
|
324
|
+
"Payment", "Order", "Booking", "User", "Hotel", "Room",
|
325
|
+
"Cart", "Price", "Guest", "Request", "Response"
|
326
|
+
],
|
327
|
+
|
328
|
+
// AI 建议(由 ai-test analyze 填充)
|
329
|
+
"suggestions": {
|
330
|
+
"businessCriticalPaths": [
|
331
|
+
// 示例:
|
332
|
+
// {
|
333
|
+
// "pattern": "services/payment/**",
|
334
|
+
// "confidence": 0.95,
|
335
|
+
// "reason": "Handles Stripe payment processing",
|
336
|
+
// "suggestedBC": 10,
|
337
|
+
// "evidence": [
|
338
|
+
// "Contains processPayment with Stripe API",
|
339
|
+
// "Referenced by checkout flow",
|
340
|
+
// "Handles money transactions"
|
341
|
+
// ]
|
342
|
+
// }
|
343
|
+
],
|
344
|
+
"highRiskModules": [
|
345
|
+
// 示例:
|
346
|
+
// {
|
347
|
+
// "pattern": "utils/date/**",
|
348
|
+
// "confidence": 0.88,
|
349
|
+
// "reason": "Complex timezone calculations",
|
350
|
+
// "suggestedER": 8,
|
351
|
+
// "evidence": [
|
352
|
+
// "Multiple timezone conversions",
|
353
|
+
// "Handles DST edge cases"
|
354
|
+
// ]
|
355
|
+
// }
|
356
|
+
],
|
357
|
+
"testabilityAdjustments": [
|
358
|
+
// 示例:
|
359
|
+
// {
|
360
|
+
// "pattern": "utils/**",
|
361
|
+
// "confidence": 0.92,
|
362
|
+
// "reason": "Pure functions, easy to test",
|
363
|
+
// "adjustment": "+1",
|
364
|
+
// "evidence": [
|
365
|
+
// "No side effects",
|
366
|
+
// "No external dependencies"
|
367
|
+
// ]
|
368
|
+
// }
|
369
|
+
]
|
370
|
+
}
|
313
371
|
}
|
314
372
|
}
|