ai-unit-test-generator 1.4.5 → 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 +138 -0
- package/README.md +307 -322
- package/bin/cli.js +100 -202
- 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
package/lib/core/scorer.mjs
CHANGED
@@ -25,11 +25,136 @@ function loadJson(p) {
|
|
25
25
|
function toFixedDown(num, digits = 2) { const m = Math.pow(10, digits); return Math.floor(num * m) / m }
|
26
26
|
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)) }
|
27
27
|
|
28
|
+
// 匹配 AI 建议的模式(支持 glob 语法)
|
29
|
+
function matchPattern(filePath, pattern) {
|
30
|
+
if (!pattern || !filePath) return false
|
31
|
+
|
32
|
+
// 移除开头的 src/ 如果存在(标准化路径)
|
33
|
+
const normalizedPath = filePath.replace(/^src\//, '')
|
34
|
+
|
35
|
+
// 简单 glob 匹配:支持 ** 和 *
|
36
|
+
const regexPattern = pattern
|
37
|
+
.replace(/\./g, '\\.') // . 转义
|
38
|
+
.replace(/\*\*/g, '__DSTAR__') // ** 临时占位
|
39
|
+
.replace(/\*/g, '[^/]*') // * 匹配非斜杠字符
|
40
|
+
.replace(/__DSTAR__/g, '.*') // ** 匹配任意字符
|
41
|
+
|
42
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
43
|
+
return regex.test(normalizedPath) || regex.test(filePath)
|
44
|
+
}
|
45
|
+
|
28
46
|
function stripJsonComments(s) {
|
29
47
|
return String(s)
|
30
48
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
31
49
|
.replace(/(^|\s)\/\/.*$/gm, '')
|
32
50
|
}
|
51
|
+
|
52
|
+
// 根据路径匹配层级(作为 fallback)
|
53
|
+
function matchLayerByPath(filePath, cfg) {
|
54
|
+
const layers = cfg?.layers || {}
|
55
|
+
|
56
|
+
// 按配置文件中定义的顺序遍历层级(foundation → business → state → ui)
|
57
|
+
const layerOrder = ['foundation', 'business', 'state', 'ui']
|
58
|
+
|
59
|
+
for (const layerKey of layerOrder) {
|
60
|
+
const layerDef = layers[layerKey]
|
61
|
+
if (!layerDef) continue
|
62
|
+
|
63
|
+
const patterns = layerDef.patterns || []
|
64
|
+
for (const pattern of patterns) {
|
65
|
+
// 简单的 glob 匹配:支持 ** 和 *
|
66
|
+
const regexPattern = pattern
|
67
|
+
.replace(/\./g, '\\.') // . 转义
|
68
|
+
.replace(/\*\*/g, '__DSTAR__') // ** 临时占位
|
69
|
+
.replace(/\*/g, '[^/]*') // * 匹配单层目录
|
70
|
+
.replace(/__DSTAR__/g, '.*') // ** 匹配任意层级目录
|
71
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
72
|
+
if (regex.test(filePath)) {
|
73
|
+
return layerKey
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
// 如果都不匹配,尝试配置中的其他层级
|
79
|
+
for (const [layerKey, layerDef] of Object.entries(layers)) {
|
80
|
+
if (layerOrder.includes(layerKey)) continue // 已经检查过了
|
81
|
+
const patterns = layerDef.patterns || []
|
82
|
+
for (const pattern of patterns) {
|
83
|
+
const regexPattern = pattern
|
84
|
+
.replace(/\./g, '\\.')
|
85
|
+
.replace(/\*\*/g, '__DSTAR__')
|
86
|
+
.replace(/\*/g, '[^/]*')
|
87
|
+
.replace(/__DSTAR__/g, '.*')
|
88
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
89
|
+
if (regex.test(filePath)) {
|
90
|
+
return layerKey
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
return 'unknown'
|
96
|
+
}
|
97
|
+
|
98
|
+
// 综合判断层级:基于代码特征(roiHint)+ 路径约定
|
99
|
+
function matchLayer(target, cfg) {
|
100
|
+
const { path, type, roiHint = {} } = target
|
101
|
+
|
102
|
+
// ===== 第一优先级:明确的状态管理路径约定 =====
|
103
|
+
// atoms/stores 是明确的架构约定,优先识别
|
104
|
+
if (path.match(/\/(atoms|stores)\//)) {
|
105
|
+
return 'state'
|
106
|
+
}
|
107
|
+
|
108
|
+
// ===== 第二优先级:基于代码特征(roiHint)=====
|
109
|
+
|
110
|
+
// UI 层:needsUI = true(代码中包含 JSX 或 React Hooks)
|
111
|
+
if (roiHint.needsUI === true) {
|
112
|
+
// 排除 context(虽然有 JSX 但本质是状态管理)
|
113
|
+
if (path.match(/\/context\//)) {
|
114
|
+
return 'state'
|
115
|
+
}
|
116
|
+
return 'ui'
|
117
|
+
}
|
118
|
+
|
119
|
+
// Foundation 层:纯函数 + 依赖可注入(真正的工具函数)
|
120
|
+
if (roiHint.isPure === true && roiHint.dependenciesInjectable === true) {
|
121
|
+
return 'foundation'
|
122
|
+
}
|
123
|
+
|
124
|
+
// Foundation 层:纯函数(即使依赖不可注入,如某些 utils)
|
125
|
+
// 但排除状态管理相关的纯函数(如 createStore)
|
126
|
+
if (roiHint.isPure === true && !path.match(/\/(atoms|stores|context)\//)) {
|
127
|
+
return 'foundation'
|
128
|
+
}
|
129
|
+
|
130
|
+
// Business 层:非纯函数 + 不需要 UI(业务逻辑、API 调用)
|
131
|
+
if (roiHint.isPure === false && roiHint.needsUI === false) {
|
132
|
+
// 排除状态管理
|
133
|
+
if (!path.match(/\/(atoms|stores|context)\//)) {
|
134
|
+
return 'business'
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
// ===== 第三优先级:基于类型 =====
|
139
|
+
|
140
|
+
// component 类型一定是 UI 层
|
141
|
+
if (type === 'component') {
|
142
|
+
return 'ui'
|
143
|
+
}
|
144
|
+
|
145
|
+
// hook 类型:根据位置判断
|
146
|
+
if (type === 'hook') {
|
147
|
+
// 在 components/pages 下的 hook 是 UI 层
|
148
|
+
if (path.match(/\/(components|pages)\//)) {
|
149
|
+
return 'ui'
|
150
|
+
}
|
151
|
+
// 独立的 hook 更可能是业务逻辑
|
152
|
+
return 'business'
|
153
|
+
}
|
154
|
+
|
155
|
+
// ===== 第四优先级:路径 fallback =====
|
156
|
+
return matchLayerByPath(path, cfg)
|
157
|
+
}
|
33
158
|
function loadConfig(pathFromArg) {
|
34
159
|
const paths = [pathFromArg, 'ai-test.config.jsonc', 'ai-test.config.json']
|
35
160
|
for (const p of paths) {
|
@@ -577,7 +702,62 @@ async function main() {
|
|
577
702
|
coverageScore = mapCoverageScore(linesPct, cfg)
|
578
703
|
}
|
579
704
|
|
580
|
-
|
705
|
+
// 根据代码特征和路径匹配层级(如果还没有匹配)
|
706
|
+
if (!t.layer || t.layer === 'unknown') {
|
707
|
+
t.layer = matchLayer(t, cfg) // 传入整个 target,而不是路径
|
708
|
+
}
|
709
|
+
|
710
|
+
// ✅ AI 增强:应用 AI 分析建议(如果启用且已分析)
|
711
|
+
let enhancedBC = BC
|
712
|
+
let enhancedER = ER
|
713
|
+
let enhancedTestability = testability
|
714
|
+
|
715
|
+
if (cfg?.aiEnhancement?.enabled && cfg?.aiEnhancement?.analyzed && cfg?.aiEnhancement?.suggestions) {
|
716
|
+
const suggestions = cfg.aiEnhancement.suggestions
|
717
|
+
|
718
|
+
// 匹配 businessCriticalPaths(提升 BC)
|
719
|
+
if (suggestions.businessCriticalPaths && Array.isArray(suggestions.businessCriticalPaths)) {
|
720
|
+
for (const item of suggestions.businessCriticalPaths) {
|
721
|
+
if (matchPattern(path, item.pattern)) {
|
722
|
+
enhancedBC = Math.max(enhancedBC, item.suggestedBC)
|
723
|
+
break // 只应用第一个匹配
|
724
|
+
}
|
725
|
+
}
|
726
|
+
}
|
727
|
+
|
728
|
+
// 匹配 highRiskModules(提升 ER)
|
729
|
+
if (suggestions.highRiskModules && Array.isArray(suggestions.highRiskModules)) {
|
730
|
+
for (const item of suggestions.highRiskModules) {
|
731
|
+
if (matchPattern(path, item.pattern)) {
|
732
|
+
enhancedER = Math.max(enhancedER, item.suggestedER)
|
733
|
+
break
|
734
|
+
}
|
735
|
+
}
|
736
|
+
}
|
737
|
+
|
738
|
+
// 匹配 testabilityAdjustments(调整 testability)
|
739
|
+
if (suggestions.testabilityAdjustments && Array.isArray(suggestions.testabilityAdjustments)) {
|
740
|
+
for (const item of suggestions.testabilityAdjustments) {
|
741
|
+
if (matchPattern(path, item.pattern)) {
|
742
|
+
const adj = parseInt(item.adjustment)
|
743
|
+
if (!isNaN(adj)) {
|
744
|
+
enhancedTestability = clamp(enhancedTestability + adj, 0, 10)
|
745
|
+
}
|
746
|
+
break
|
747
|
+
}
|
748
|
+
}
|
749
|
+
}
|
750
|
+
}
|
751
|
+
|
752
|
+
let { score, priority, layer, layerName } = computeScore({
|
753
|
+
BC: enhancedBC,
|
754
|
+
CC: CCFinal,
|
755
|
+
ER: enhancedER,
|
756
|
+
ROI,
|
757
|
+
testability: enhancedTestability,
|
758
|
+
dependencyCount,
|
759
|
+
coverageScore
|
760
|
+
}, t, cfg)
|
581
761
|
|
582
762
|
// 覆盖率加权(可选):对低覆盖文件小幅加分
|
583
763
|
if (coverageSummary) {
|
@@ -605,11 +785,11 @@ async function main() {
|
|
605
785
|
type,
|
606
786
|
layer: layer || 'N/A',
|
607
787
|
layerName: layerName || 'N/A',
|
608
|
-
BC,
|
788
|
+
BC: enhancedBC, // 使用 AI 增强后的值
|
609
789
|
CC: CCFinal,
|
610
|
-
ER,
|
790
|
+
ER: enhancedER, // 使用 AI 增强后的值
|
611
791
|
ROI,
|
612
|
-
testability,
|
792
|
+
testability: enhancedTestability, // 使用 AI 增强后的值
|
613
793
|
dependencyCount,
|
614
794
|
coveragePct: coveragePct ?? 'N/A',
|
615
795
|
coverageScore: coverageScore ?? 'N/A',
|
@@ -0,0 +1,110 @@
|
|
1
|
+
/**
|
2
|
+
* 配置文件管理工具
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
|
6
|
+
import { join } from 'node:path'
|
7
|
+
import { fileURLToPath } from 'node:url'
|
8
|
+
import { dirname } from 'node:path'
|
9
|
+
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
11
|
+
const __dirname = dirname(__filename)
|
12
|
+
const PKG_ROOT = join(__dirname, '../..')
|
13
|
+
|
14
|
+
/**
|
15
|
+
* 移除 JSON 注释
|
16
|
+
*/
|
17
|
+
function stripJsonComments(str) {
|
18
|
+
return String(str)
|
19
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // 块注释
|
20
|
+
.replace(/(^|\s)\/\/.*$/gm, '') // 行注释
|
21
|
+
}
|
22
|
+
|
23
|
+
/**
|
24
|
+
* 自动探测配置文件
|
25
|
+
*/
|
26
|
+
export function detectConfig(providedPath) {
|
27
|
+
const detectOrder = [
|
28
|
+
providedPath,
|
29
|
+
'ai-test.config.jsonc',
|
30
|
+
'ai-test.config.json',
|
31
|
+
'ut_scoring_config.json' // 向后兼容
|
32
|
+
].filter(Boolean)
|
33
|
+
|
34
|
+
return detectOrder.find(p => existsSync(p)) || null
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* 确保配置文件存在(不存在则创建)
|
39
|
+
*/
|
40
|
+
export function ensureConfig(configPath = 'ai-test.config.jsonc') {
|
41
|
+
const detected = detectConfig(configPath)
|
42
|
+
|
43
|
+
if (detected) {
|
44
|
+
return detected
|
45
|
+
}
|
46
|
+
|
47
|
+
// 创建默认配置
|
48
|
+
console.log('⚙️ Config not found, creating default config...')
|
49
|
+
const templatePath = join(PKG_ROOT, 'templates', 'default.config.jsonc')
|
50
|
+
copyFileSync(templatePath, configPath)
|
51
|
+
console.log(`✅ Config created: ${configPath}\n`)
|
52
|
+
|
53
|
+
return configPath
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* 读取配置文件(支持 JSONC)
|
58
|
+
*/
|
59
|
+
export function readConfig(configPath) {
|
60
|
+
if (!existsSync(configPath)) {
|
61
|
+
throw new Error(`Config file not found: ${configPath}`)
|
62
|
+
}
|
63
|
+
|
64
|
+
const content = readFileSync(configPath, 'utf-8')
|
65
|
+
const cleaned = stripJsonComments(content)
|
66
|
+
|
67
|
+
try {
|
68
|
+
return JSON.parse(cleaned)
|
69
|
+
} catch (err) {
|
70
|
+
throw new Error(`Failed to parse config file: ${configPath}\n${err.message}`)
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
/**
|
75
|
+
* 写入配置文件(保留 JSONC 格式)
|
76
|
+
*/
|
77
|
+
export function writeConfig(configPath, config) {
|
78
|
+
const content = JSON.stringify(config, null, 2)
|
79
|
+
writeFileSync(configPath, content, 'utf-8')
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* 检查配置是否已经 AI 分析过
|
84
|
+
*/
|
85
|
+
export function isAnalyzed(config) {
|
86
|
+
return config?.aiEnhancement?.analyzed === true
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* 验证配置结构
|
91
|
+
*/
|
92
|
+
export function validateConfig(config) {
|
93
|
+
const errors = []
|
94
|
+
|
95
|
+
// 检查必需字段
|
96
|
+
if (!config.scoringMode) {
|
97
|
+
errors.push('Missing required field: scoringMode')
|
98
|
+
}
|
99
|
+
|
100
|
+
if (config.scoringMode === 'layered' && !config.layers) {
|
101
|
+
errors.push('Layered mode requires "layers" field')
|
102
|
+
}
|
103
|
+
|
104
|
+
if (config.scoringMode === 'legacy' && (!config.weights || !config.thresholds)) {
|
105
|
+
errors.push('Legacy mode requires "weights" and "thresholds" fields')
|
106
|
+
}
|
107
|
+
|
108
|
+
return errors
|
109
|
+
}
|
110
|
+
|
package/lib/utils/index.mjs
CHANGED
@@ -0,0 +1,121 @@
|
|
1
|
+
/**
|
2
|
+
* 扫描结果管理工具
|
3
|
+
*/
|
4
|
+
|
5
|
+
import { existsSync, statSync, readdirSync } from 'node:fs'
|
6
|
+
import { join } from 'node:path'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 检查源代码是否有变化
|
10
|
+
*/
|
11
|
+
export function hasSourceCodeChanged(targetsPath, srcDir = 'src') {
|
12
|
+
if (!existsSync(targetsPath)) {
|
13
|
+
return true
|
14
|
+
}
|
15
|
+
|
16
|
+
const targetsTime = statSync(targetsPath).mtimeMs
|
17
|
+
return checkDirectoryRecursive(srcDir, targetsTime)
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* 递归检查目录中的文件修改时间
|
22
|
+
*/
|
23
|
+
function checkDirectoryRecursive(dir, compareTime) {
|
24
|
+
if (!existsSync(dir)) {
|
25
|
+
return false
|
26
|
+
}
|
27
|
+
|
28
|
+
try {
|
29
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
30
|
+
|
31
|
+
for (const entry of entries) {
|
32
|
+
const fullPath = join(dir, entry.name)
|
33
|
+
|
34
|
+
if (entry.isDirectory()) {
|
35
|
+
// 跳过不需要检查的目录
|
36
|
+
if (['node_modules', 'dist', '.git', 'reports', 'coverage'].includes(entry.name)) {
|
37
|
+
continue
|
38
|
+
}
|
39
|
+
|
40
|
+
if (checkDirectoryRecursive(fullPath, compareTime)) {
|
41
|
+
return true
|
42
|
+
}
|
43
|
+
} else if (entry.isFile()) {
|
44
|
+
// 只检查代码文件
|
45
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
46
|
+
continue
|
47
|
+
}
|
48
|
+
|
49
|
+
const fileTime = statSync(fullPath).mtimeMs
|
50
|
+
if (fileTime > compareTime) {
|
51
|
+
console.log(` Detected change: ${fullPath}`)
|
52
|
+
return true
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
} catch (err) {
|
57
|
+
// 忽略权限错误等
|
58
|
+
console.warn(` Warning: Could not access ${dir}`)
|
59
|
+
}
|
60
|
+
|
61
|
+
return false
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* 检查配置文件是否有变化
|
66
|
+
*/
|
67
|
+
export function hasConfigChanged(configPath, scoresPath) {
|
68
|
+
if (!existsSync(configPath) || !existsSync(scoresPath)) {
|
69
|
+
return false
|
70
|
+
}
|
71
|
+
|
72
|
+
return statSync(configPath).mtimeMs > statSync(scoresPath).mtimeMs
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* 检查是否需要重新扫描
|
77
|
+
* @returns {boolean}
|
78
|
+
*/
|
79
|
+
export function needsRescan(options) {
|
80
|
+
const { force, targetsPath, scoresPath, configPath } = options
|
81
|
+
|
82
|
+
// 1. 强制重扫
|
83
|
+
if (force) {
|
84
|
+
return true
|
85
|
+
}
|
86
|
+
|
87
|
+
// 2. 目标文件不存在
|
88
|
+
if (!existsSync(targetsPath)) {
|
89
|
+
return true
|
90
|
+
}
|
91
|
+
|
92
|
+
// 3. 打分结果不存在
|
93
|
+
if (scoresPath && !existsSync(scoresPath)) {
|
94
|
+
return true
|
95
|
+
}
|
96
|
+
|
97
|
+
// 4. 源代码有变化
|
98
|
+
if (hasSourceCodeChanged(targetsPath)) {
|
99
|
+
return true
|
100
|
+
}
|
101
|
+
|
102
|
+
// 5. 配置文件有变化
|
103
|
+
if (configPath && scoresPath && hasConfigChanged(configPath, scoresPath)) {
|
104
|
+
return true
|
105
|
+
}
|
106
|
+
|
107
|
+
return false
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* 获取文件的年龄(小时)
|
112
|
+
*/
|
113
|
+
export function getFileAgeHours(filePath) {
|
114
|
+
if (!existsSync(filePath)) {
|
115
|
+
return Infinity
|
116
|
+
}
|
117
|
+
|
118
|
+
const stats = statSync(filePath)
|
119
|
+
return (Date.now() - stats.mtimeMs) / (1000 * 60 * 60)
|
120
|
+
}
|
121
|
+
|
@@ -0,0 +1,157 @@
|
|
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 responsePath = 'reports/ai_analyze_response.txt'
|
126
|
+
|
127
|
+
const child = spawn('cursor-agent', [
|
128
|
+
'--prompt-file', promptPath,
|
129
|
+
'--output', responsePath
|
130
|
+
], {
|
131
|
+
stdio: 'inherit',
|
132
|
+
shell: true,
|
133
|
+
cwd: process.cwd()
|
134
|
+
})
|
135
|
+
|
136
|
+
child.on('close', (code) => {
|
137
|
+
if (code !== 0) {
|
138
|
+
reject(new Error(`cursor-agent exited with code ${code}`))
|
139
|
+
return
|
140
|
+
}
|
141
|
+
|
142
|
+
// 读取响应
|
143
|
+
try {
|
144
|
+
const { readFileSync } = require('node:fs')
|
145
|
+
const response = readFileSync(responsePath, 'utf-8')
|
146
|
+
resolve(response)
|
147
|
+
} catch (err) {
|
148
|
+
reject(new Error(`Failed to read AI response: ${err.message}`))
|
149
|
+
}
|
150
|
+
})
|
151
|
+
|
152
|
+
child.on('error', (err) => {
|
153
|
+
reject(new Error(`Failed to spawn cursor-agent: ${err.message}`))
|
154
|
+
})
|
155
|
+
})
|
156
|
+
}
|
157
|
+
|