ai-unit-test-generator 1.4.6 → 2.0.1
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 +131 -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 +157 -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,99 @@
|
|
1
|
+
/**
|
2
|
+
* 安全的配置写入器
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { readConfig, writeConfig } from '../utils/config-manager.mjs'
|
6
|
+
|
7
|
+
const LOCKED_PATHS = [
|
8
|
+
'scoringMode',
|
9
|
+
'layers',
|
10
|
+
'weights',
|
11
|
+
'thresholds',
|
12
|
+
'ccMapping',
|
13
|
+
'dependencyCountMapping',
|
14
|
+
'coverageScoring',
|
15
|
+
'fallbacks',
|
16
|
+
'aiEnhancement.enabled'
|
17
|
+
]
|
18
|
+
|
19
|
+
const WRITABLE_PATHS = [
|
20
|
+
'aiEnhancement.analyzed',
|
21
|
+
'aiEnhancement.analyzedAt',
|
22
|
+
'aiEnhancement.suggestions'
|
23
|
+
]
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 应用 AI 建议到配置文件
|
27
|
+
*/
|
28
|
+
export async function applyAISuggestions(configPath, suggestions) {
|
29
|
+
// 1. 读取现有配置
|
30
|
+
const config = readConfig(configPath)
|
31
|
+
|
32
|
+
// 2. 验证权限
|
33
|
+
validateWritePermissions(suggestions)
|
34
|
+
|
35
|
+
// 3. 深拷贝配置
|
36
|
+
const newConfig = JSON.parse(JSON.stringify(config))
|
37
|
+
|
38
|
+
// 4. 初始化 aiEnhancement(如果不存在)
|
39
|
+
if (!newConfig.aiEnhancement) {
|
40
|
+
newConfig.aiEnhancement = {
|
41
|
+
enabled: true,
|
42
|
+
analyzed: false,
|
43
|
+
analyzedAt: null,
|
44
|
+
suggestions: {
|
45
|
+
businessCriticalPaths: [],
|
46
|
+
highRiskModules: [],
|
47
|
+
testabilityAdjustments: []
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
// 5. 只写入允许的字段
|
53
|
+
newConfig.aiEnhancement.analyzed = true
|
54
|
+
newConfig.aiEnhancement.analyzedAt = new Date().toISOString()
|
55
|
+
newConfig.aiEnhancement.suggestions = suggestions
|
56
|
+
|
57
|
+
// 6. 验证核心配置未被修改
|
58
|
+
validateCoreConfigIntact(config, newConfig)
|
59
|
+
|
60
|
+
// 7. 写入文件
|
61
|
+
writeConfig(configPath, newConfig)
|
62
|
+
|
63
|
+
return newConfig
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* 验证写入权限
|
68
|
+
*/
|
69
|
+
function validateWritePermissions(suggestions) {
|
70
|
+
const allowedKeys = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
|
71
|
+
|
72
|
+
for (const key of Object.keys(suggestions)) {
|
73
|
+
if (!allowedKeys.includes(key)) {
|
74
|
+
throw new Error(`AI attempted to write forbidden field: ${key}`)
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* 验证核心配置完整性
|
81
|
+
*/
|
82
|
+
function validateCoreConfigIntact(oldConfig, newConfig) {
|
83
|
+
for (const path of LOCKED_PATHS) {
|
84
|
+
const oldValue = getNestedValue(oldConfig, path)
|
85
|
+
const newValue = getNestedValue(newConfig, path)
|
86
|
+
|
87
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
88
|
+
throw new Error(`Core config was modified (locked path: ${path})`)
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
/**
|
94
|
+
* 获取嵌套对象的值
|
95
|
+
*/
|
96
|
+
function getNestedValue(obj, path) {
|
97
|
+
return path.split('.').reduce((curr, key) => curr?.[key], obj)
|
98
|
+
}
|
99
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
/**
|
2
|
+
* 项目上下文构建工具
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 构建项目上下文信息
|
9
|
+
*/
|
10
|
+
export async function buildProjectContext() {
|
11
|
+
const context = {
|
12
|
+
framework: 'Unknown',
|
13
|
+
criticalDeps: [],
|
14
|
+
devDeps: []
|
15
|
+
}
|
16
|
+
|
17
|
+
// 读取 package.json
|
18
|
+
if (existsSync('package.json')) {
|
19
|
+
try {
|
20
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
21
|
+
|
22
|
+
context.name = pkg.name
|
23
|
+
context.framework = detectFramework(pkg.dependencies || {})
|
24
|
+
|
25
|
+
// 识别关键依赖
|
26
|
+
const deps = Object.keys(pkg.dependencies || {})
|
27
|
+
const criticalKeywords = ['stripe', 'payment', 'auth', 'prisma', 'db', 'axios', 'fetch', 'jotai', 'zustand', 'redux']
|
28
|
+
|
29
|
+
context.criticalDeps = deps.filter(dep =>
|
30
|
+
criticalKeywords.some(kw => dep.toLowerCase().includes(kw))
|
31
|
+
)
|
32
|
+
|
33
|
+
context.devDeps = Object.keys(pkg.devDependencies || {})
|
34
|
+
} catch (err) {
|
35
|
+
console.warn('Warning: Could not read package.json')
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
return context
|
40
|
+
}
|
41
|
+
|
42
|
+
/**
|
43
|
+
* 检测项目框架
|
44
|
+
*/
|
45
|
+
function detectFramework(deps) {
|
46
|
+
if (deps['next']) return 'Next.js'
|
47
|
+
if (deps['react']) return 'React'
|
48
|
+
if (deps['vue']) return 'Vue'
|
49
|
+
if (deps['@angular/core']) return 'Angular'
|
50
|
+
return 'Node.js'
|
51
|
+
}
|
52
|
+
|
package/lib/ai/index.mjs
CHANGED
@@ -3,9 +3,20 @@
|
|
3
3
|
*
|
4
4
|
* 提供 Prompt 构建、AI 调用和测试提取功能
|
5
5
|
* 支持多种 LLM(目前实现:cursor-agent)
|
6
|
+
*
|
7
|
+
* AI 分析功能:代码库分析、配置优化建议
|
6
8
|
*/
|
7
9
|
|
10
|
+
// 测试生成相关
|
8
11
|
export { buildBatchPrompt, runCLI as buildPrompt } from './prompt-builder.mjs'
|
9
12
|
export { main as callAI } from './client.mjs'
|
10
13
|
export { extractTests } from './extractor.mjs'
|
11
14
|
|
15
|
+
// AI 分析相关
|
16
|
+
export * from './sampler.mjs'
|
17
|
+
export * from './context-builder.mjs'
|
18
|
+
export * from './analyzer-prompt.mjs'
|
19
|
+
export * from './validator.mjs'
|
20
|
+
export * from './reviewer.mjs'
|
21
|
+
export * from './config-writer.mjs'
|
22
|
+
|
@@ -0,0 +1,283 @@
|
|
1
|
+
/**
|
2
|
+
* 交互式 AI 建议审核器
|
3
|
+
* 支持分类审核、逐条选择、批量调整分数
|
4
|
+
*/
|
5
|
+
|
6
|
+
import readline from 'readline'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 创建 readline 接口
|
10
|
+
*/
|
11
|
+
function createInterface() {
|
12
|
+
return readline.createInterface({
|
13
|
+
input: process.stdin,
|
14
|
+
output: process.stdout
|
15
|
+
})
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* 询问用户输入
|
20
|
+
*/
|
21
|
+
function ask(rl, question) {
|
22
|
+
return new Promise(resolve => rl.question(question, resolve))
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 获取分类图标
|
27
|
+
*/
|
28
|
+
function getCategoryIcon(category) {
|
29
|
+
const icons = {
|
30
|
+
businessCriticalPaths: '🔴',
|
31
|
+
highRiskModules: '⚠️',
|
32
|
+
testabilityAdjustments: '✅'
|
33
|
+
}
|
34
|
+
return icons[category] || '📝'
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* 获取分类名称
|
39
|
+
*/
|
40
|
+
function getCategoryName(category) {
|
41
|
+
const names = {
|
42
|
+
businessCriticalPaths: 'Business Critical Paths',
|
43
|
+
highRiskModules: 'High Risk Modules',
|
44
|
+
testabilityAdjustments: 'Testability Adjustments'
|
45
|
+
}
|
46
|
+
return names[category] || category
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* 获取分类名称(小写)
|
51
|
+
*/
|
52
|
+
function getCategoryNameLower(category) {
|
53
|
+
const names = {
|
54
|
+
businessCriticalPaths: 'business critical paths',
|
55
|
+
highRiskModules: 'high risk modules',
|
56
|
+
testabilityAdjustments: 'testability adjustments'
|
57
|
+
}
|
58
|
+
return names[category] || category
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* 格式化单个建议
|
63
|
+
*/
|
64
|
+
function formatSuggestion(item, index, category) {
|
65
|
+
let output = `\n [${index}] ${item.pattern}\n`
|
66
|
+
|
67
|
+
if (category === 'businessCriticalPaths') {
|
68
|
+
output += ` BC: ${item.suggestedBC} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
69
|
+
} else if (category === 'highRiskModules') {
|
70
|
+
output += ` ER: ${item.suggestedER} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
71
|
+
} else if (category === 'testabilityAdjustments') {
|
72
|
+
output += ` Adjustment: ${item.adjustment} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
73
|
+
}
|
74
|
+
|
75
|
+
output += ` Reason: ${item.reason}\n`
|
76
|
+
output += ` Evidence:\n`
|
77
|
+
item.evidence.forEach(e => {
|
78
|
+
output += ` - ${e}\n`
|
79
|
+
})
|
80
|
+
|
81
|
+
return output
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* 显示建议列表
|
86
|
+
*/
|
87
|
+
function displaySuggestions(category, items) {
|
88
|
+
const icon = getCategoryIcon(category)
|
89
|
+
const name = getCategoryName(category)
|
90
|
+
|
91
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
92
|
+
console.log(`${icon} ${name} (${items.length} suggestions):`)
|
93
|
+
|
94
|
+
items.forEach((item, i) => {
|
95
|
+
console.log(formatSuggestion(item, i + 1, category))
|
96
|
+
})
|
97
|
+
|
98
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* 解析用户选择
|
103
|
+
*/
|
104
|
+
function parseSelection(input, maxCount) {
|
105
|
+
if (!input || input.trim() === '') {
|
106
|
+
return []
|
107
|
+
}
|
108
|
+
|
109
|
+
return input.split(',')
|
110
|
+
.map(s => parseInt(s.trim()))
|
111
|
+
.filter(n => !isNaN(n) && n >= 1 && n <= maxCount)
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* 批量调整分数
|
116
|
+
*/
|
117
|
+
async function adjustScores(rl, items, category) {
|
118
|
+
console.log(`\nAdjust scores (press Enter to keep original):`)
|
119
|
+
|
120
|
+
const scoreField = category === 'businessCriticalPaths' ? 'suggestedBC' : 'suggestedER'
|
121
|
+
const minScore = category === 'businessCriticalPaths' ? 8 : 7
|
122
|
+
const maxScore = 10
|
123
|
+
|
124
|
+
const adjusted = []
|
125
|
+
|
126
|
+
for (let i = 0; i < items.length; i++) {
|
127
|
+
const item = { ...items[i] }
|
128
|
+
const currentScore = item[scoreField]
|
129
|
+
|
130
|
+
const input = await ask(rl, ` [${i + 1}] ${item.pattern} (${scoreField}: ${currentScore}): `)
|
131
|
+
|
132
|
+
if (input && input.trim() !== '') {
|
133
|
+
const newScore = parseInt(input.trim())
|
134
|
+
if (!isNaN(newScore) && newScore >= minScore && newScore <= maxScore) {
|
135
|
+
item[scoreField] = newScore
|
136
|
+
console.log(` ✅ Updated: ${currentScore} → ${newScore}`)
|
137
|
+
} else {
|
138
|
+
console.log(` ⚠️ Invalid score (must be ${minScore}-${maxScore}), keeping original`)
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
adjusted.push(item)
|
143
|
+
}
|
144
|
+
|
145
|
+
return adjusted
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* 审核单个分类
|
150
|
+
*/
|
151
|
+
async function reviewCategory(rl, category, items) {
|
152
|
+
if (items.length === 0) {
|
153
|
+
return []
|
154
|
+
}
|
155
|
+
|
156
|
+
// 显示建议
|
157
|
+
displaySuggestions(category, items)
|
158
|
+
|
159
|
+
// 询问操作
|
160
|
+
const categoryName = getCategoryNameLower(category)
|
161
|
+
const action = await ask(rl,
|
162
|
+
`❓ Review ${categoryName}:\n` +
|
163
|
+
` [a] Accept all (${items.length})\n` +
|
164
|
+
` [r] Reject all\n` +
|
165
|
+
` [s] Select individually\n` +
|
166
|
+
` [n] Skip (keep empty)\n` +
|
167
|
+
`> `
|
168
|
+
)
|
169
|
+
|
170
|
+
const actionLower = action.trim().toLowerCase()
|
171
|
+
|
172
|
+
if (actionLower === 'a') {
|
173
|
+
// 接受全部
|
174
|
+
console.log(`✅ Accepted: ${items.length}/${items.length} ${categoryName}`)
|
175
|
+
|
176
|
+
// 询问是否调整分数
|
177
|
+
if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
|
178
|
+
const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
|
179
|
+
if (adjustInput.trim().toLowerCase() === 'y') {
|
180
|
+
return await adjustScores(rl, items, category)
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
return items
|
185
|
+
} else if (actionLower === 's') {
|
186
|
+
// 逐条选择
|
187
|
+
const selection = await ask(rl, `\nSelect which suggestions to accept (comma-separated, e.g. 1,3):\n> `)
|
188
|
+
const indices = parseSelection(selection, items.length)
|
189
|
+
|
190
|
+
if (indices.length === 0) {
|
191
|
+
console.log(`❌ No suggestions selected`)
|
192
|
+
return []
|
193
|
+
}
|
194
|
+
|
195
|
+
const selected = indices.map(i => items[i - 1])
|
196
|
+
console.log(`✅ Selected: ${selected.length}/${items.length} ${categoryName}`)
|
197
|
+
|
198
|
+
// 询问是否调整分数
|
199
|
+
if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
|
200
|
+
const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
|
201
|
+
if (adjustInput.trim().toLowerCase() === 'y') {
|
202
|
+
return await adjustScores(rl, selected, category)
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
return selected
|
207
|
+
} else if (actionLower === 'r') {
|
208
|
+
// 拒绝全部
|
209
|
+
console.log(`❌ Rejected: all ${categoryName}`)
|
210
|
+
return []
|
211
|
+
} else {
|
212
|
+
// 跳过
|
213
|
+
console.log(`⏭️ Skipped ${categoryName}`)
|
214
|
+
return []
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
/**
|
219
|
+
* 显示总结
|
220
|
+
*/
|
221
|
+
function displaySummary(result, validated) {
|
222
|
+
const totalSuggested = Object.values(validated).reduce((sum, arr) => sum + arr.length, 0)
|
223
|
+
const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
|
224
|
+
|
225
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
226
|
+
console.log(`📊 Summary:`)
|
227
|
+
console.log(` Business Critical Paths: ${result.businessCriticalPaths.length}/${validated.businessCriticalPaths?.length || 0} accepted`)
|
228
|
+
console.log(` High Risk Modules: ${result.highRiskModules.length}/${validated.highRiskModules?.length || 0} accepted`)
|
229
|
+
console.log(` Testability Adjustments: ${result.testabilityAdjustments.length}/${validated.testabilityAdjustments?.length || 0} accepted`)
|
230
|
+
console.log(` Total: ${totalAccepted}/${totalSuggested} suggestions accepted`)
|
231
|
+
|
232
|
+
if (totalAccepted === 0) {
|
233
|
+
console.log(`\n⚠️ No suggestions accepted. Config will not be modified.`)
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
/**
|
238
|
+
* 交互式审核
|
239
|
+
* @param {Object} validated - 已验证的建议
|
240
|
+
* @returns {Object|null} - 用户批准的建议,或 null(取消)
|
241
|
+
*/
|
242
|
+
export async function interactiveReview(validated) {
|
243
|
+
const rl = createInterface()
|
244
|
+
|
245
|
+
const result = {
|
246
|
+
businessCriticalPaths: [],
|
247
|
+
highRiskModules: [],
|
248
|
+
testabilityAdjustments: []
|
249
|
+
}
|
250
|
+
|
251
|
+
try {
|
252
|
+
// 审核每个分类
|
253
|
+
const categories = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
|
254
|
+
|
255
|
+
for (const category of categories) {
|
256
|
+
const items = validated[category] || []
|
257
|
+
if (items.length === 0) continue
|
258
|
+
|
259
|
+
result[category] = await reviewCategory(rl, category, items)
|
260
|
+
}
|
261
|
+
|
262
|
+
// 显示总结
|
263
|
+
displaySummary(result, validated)
|
264
|
+
|
265
|
+
const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
|
266
|
+
|
267
|
+
if (totalAccepted === 0) {
|
268
|
+
rl.close()
|
269
|
+
return null
|
270
|
+
}
|
271
|
+
|
272
|
+
// 最终确认
|
273
|
+
const save = await ask(rl, `\n💾 Save these changes to ai-test.config.jsonc? (y/n)\n> `)
|
274
|
+
|
275
|
+
rl.close()
|
276
|
+
|
277
|
+
return save.trim().toLowerCase() === 'y' ? result : null
|
278
|
+
} catch (err) {
|
279
|
+
rl.close()
|
280
|
+
throw err
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
/**
|
2
|
+
* 代码文件采样工具
|
3
|
+
* 智能选择代表性代码文件供 AI 分析
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { readFileSync, statSync } from 'node:fs'
|
7
|
+
import fg from 'fast-glob'
|
8
|
+
|
9
|
+
/**
|
10
|
+
* 智能采样代码文件
|
11
|
+
* @returns {Array} 采样结果
|
12
|
+
*/
|
13
|
+
export async function sampleCodeFiles() {
|
14
|
+
const samples = []
|
15
|
+
|
16
|
+
// 策略 1: 按目录分层采样(确保覆盖各层)
|
17
|
+
const layers = {
|
18
|
+
services: await fg('src/services/**/*.{ts,tsx}'),
|
19
|
+
components: await fg('src/components/**/*.{ts,tsx}'),
|
20
|
+
utils: await fg('src/utils/**/*.{ts,tsx}'),
|
21
|
+
atoms: await fg('src/atoms/**/*.{ts,tsx}'),
|
22
|
+
stores: await fg('src/stores/**/*.{ts,tsx}'),
|
23
|
+
hooks: await fg('src/hooks/**/*.{ts,tsx}'),
|
24
|
+
context: await fg('src/context/**/*.{ts,tsx}')
|
25
|
+
}
|
26
|
+
|
27
|
+
for (const [layer, files] of Object.entries(layers)) {
|
28
|
+
if (files.length === 0) continue
|
29
|
+
|
30
|
+
// 每层选 2-3 个文件
|
31
|
+
const selected = files
|
32
|
+
.map(f => ({
|
33
|
+
path: f,
|
34
|
+
size: statSync(f).size,
|
35
|
+
name: f.split('/').pop()
|
36
|
+
}))
|
37
|
+
.sort((a, b) => b.size - a.size) // 按文件大小排序
|
38
|
+
.slice(0, 3)
|
39
|
+
|
40
|
+
for (const file of selected) {
|
41
|
+
samples.push({
|
42
|
+
path: file.path,
|
43
|
+
layer,
|
44
|
+
reason: `${layer}_representative`,
|
45
|
+
preview: readFileSync(file.path, 'utf-8').slice(0, 1500) // 前1500字符
|
46
|
+
})
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// 策略 2: 关键词匹配(业务关键代码)
|
51
|
+
const keywords = ['payment', 'order', 'booking', 'checkout', 'price', 'cart', 'hotel', 'room']
|
52
|
+
const criticalFiles = await fg('src/**/*.{ts,tsx}')
|
53
|
+
|
54
|
+
for (const keyword of keywords) {
|
55
|
+
const matched = criticalFiles.filter(f => f.toLowerCase().includes(keyword))
|
56
|
+
if (matched.length > 0) {
|
57
|
+
const file = matched[0] // 取第一个
|
58
|
+
if (!samples.find(s => s.path === file)) {
|
59
|
+
samples.push({
|
60
|
+
path: file,
|
61
|
+
layer: 'business',
|
62
|
+
reason: `critical_keyword_${keyword}`,
|
63
|
+
preview: readFileSync(file, 'utf-8').slice(0, 1500)
|
64
|
+
})
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
// 去重
|
70
|
+
const uniqueSamples = samples.filter((s, i, arr) =>
|
71
|
+
arr.findIndex(x => x.path === s.path) === i
|
72
|
+
)
|
73
|
+
|
74
|
+
return uniqueSamples.slice(0, 25) // 最多 25 个样本
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* 分析项目结构
|
79
|
+
*/
|
80
|
+
export async function analyzeProjectStructure() {
|
81
|
+
const allFiles = await fg('src/**/*.{ts,tsx,js,jsx}')
|
82
|
+
|
83
|
+
let totalLines = 0
|
84
|
+
for (const file of allFiles) {
|
85
|
+
try {
|
86
|
+
const content = readFileSync(file, 'utf-8')
|
87
|
+
totalLines += content.split('\n').length
|
88
|
+
} catch {}
|
89
|
+
}
|
90
|
+
|
91
|
+
return {
|
92
|
+
totalFiles: allFiles.length,
|
93
|
+
totalLines,
|
94
|
+
avgLinesPerFile: Math.round(totalLines / allFiles.length)
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
/**
|
2
|
+
* AI 响应验证器
|
3
|
+
*/
|
4
|
+
|
5
|
+
const SCHEMA = {
|
6
|
+
businessCriticalPaths: {
|
7
|
+
minConfidence: 0.85,
|
8
|
+
maxCount: 10,
|
9
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'suggestedBC', 'evidence'],
|
10
|
+
validators: {
|
11
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
12
|
+
confidence: (v) => v >= 0.85 && v <= 1.0,
|
13
|
+
suggestedBC: (v) => [8, 9, 10].includes(v),
|
14
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
15
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
16
|
+
}
|
17
|
+
},
|
18
|
+
highRiskModules: {
|
19
|
+
minConfidence: 0.75,
|
20
|
+
maxCount: 10,
|
21
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'suggestedER', 'evidence'],
|
22
|
+
validators: {
|
23
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
24
|
+
confidence: (v) => v >= 0.75 && v <= 1.0,
|
25
|
+
suggestedER: (v) => [7, 8, 9, 10].includes(v),
|
26
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
27
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
28
|
+
}
|
29
|
+
},
|
30
|
+
testabilityAdjustments: {
|
31
|
+
minConfidence: 0.80,
|
32
|
+
maxCount: 10,
|
33
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'adjustment', 'evidence'],
|
34
|
+
validators: {
|
35
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
36
|
+
confidence: (v) => v >= 0.80 && v <= 1.0,
|
37
|
+
adjustment: (v) => ['-2', '-1', '+1', '+2'].includes(v),
|
38
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
39
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* 验证单个建议
|
46
|
+
*/
|
47
|
+
function validateSuggestion(item, schema) {
|
48
|
+
// 检查必需字段
|
49
|
+
for (const field of schema.requiredFields) {
|
50
|
+
if (!(field in item)) {
|
51
|
+
return false
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
// 检查置信度
|
56
|
+
if (item.confidence < schema.minConfidence) {
|
57
|
+
return false
|
58
|
+
}
|
59
|
+
|
60
|
+
// 运行字段验证器
|
61
|
+
for (const [field, validator] of Object.entries(schema.validators)) {
|
62
|
+
if (field in item && !validator(item[field])) {
|
63
|
+
return false
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
return true
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* 验证并清洗 AI 响应
|
72
|
+
*/
|
73
|
+
export function validateAndSanitize(parsed) {
|
74
|
+
const result = {
|
75
|
+
businessCriticalPaths: [],
|
76
|
+
highRiskModules: [],
|
77
|
+
testabilityAdjustments: []
|
78
|
+
}
|
79
|
+
|
80
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.suggestions) {
|
81
|
+
console.warn('⚠️ Invalid AI response structure')
|
82
|
+
return result
|
83
|
+
}
|
84
|
+
|
85
|
+
const suggestions = parsed.suggestions
|
86
|
+
|
87
|
+
for (const [key, items] of Object.entries(suggestions)) {
|
88
|
+
const schema = SCHEMA[key]
|
89
|
+
if (!schema || !Array.isArray(items)) continue
|
90
|
+
|
91
|
+
const validated = items
|
92
|
+
.filter(item => validateSuggestion(item, schema))
|
93
|
+
.sort((a, b) => b.confidence - a.confidence)
|
94
|
+
.slice(0, schema.maxCount)
|
95
|
+
|
96
|
+
result[key] = validated
|
97
|
+
}
|
98
|
+
|
99
|
+
return result
|
100
|
+
}
|
101
|
+
|