ai-unit-test-generator 1.4.1 → 1.4.3
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 +11 -0
- package/bin/cli.js +6 -3
- package/lib/core/scanner.mjs +6 -1
- package/lib/core/scorer.mjs +25 -31
- package/package.json +1 -1
- package/templates/default.config.jsonc +314 -0
- package/lib/extract-tests.mjs +0 -199
- package/lib/index.js +0 -18
- package/lib/run-batch.mjs +0 -81
- package/templates/default.config.json +0 -227
package/CHANGELOG.md
CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.4.3] - 2025-01-10
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
- **Config file format**: Changed default config from `.json` to `.jsonc` with comprehensive inline comments
|
12
|
+
- **Report simplification**: Removed redundant `Notes` column from reports (info now in config comments)
|
13
|
+
- **Better documentation**: Config file now self-documenting with detailed explanations of each scoring parameter
|
14
|
+
- Auto-detection order: `ai-test.config.jsonc` → `ai-test.config.json` → `ut_scoring_config.json` (deprecated)
|
15
|
+
|
16
|
+
### Fixed
|
17
|
+
- Config file now properly supports JSONC format (JSON with comments)
|
18
|
+
|
8
19
|
## [1.0.0] - 2025-01-10
|
9
20
|
|
10
21
|
### Breaking Changes
|
package/bin/cli.js
CHANGED
@@ -24,13 +24,16 @@ program
|
|
24
24
|
program
|
25
25
|
.command('scan')
|
26
26
|
.description('Scan code and generate priority scoring report')
|
27
|
-
.option('-c, --config <path>', 'Config file path', '
|
27
|
+
.option('-c, --config <path>', 'Config file path', 'ai-test.config.jsonc')
|
28
28
|
.option('-o, --output <dir>', 'Output directory', 'reports')
|
29
29
|
.option('--skip-git', 'Skip Git signals generation')
|
30
30
|
.action(async (options) => {
|
31
|
-
|
31
|
+
let { config, output, skipGit } = options
|
32
32
|
|
33
|
-
//
|
33
|
+
// 自动探测现有配置 & 初始化(不存在则创建)
|
34
|
+
const detectOrder = [config, 'ai-test.config.json', 'ai-test.config.jsonc']
|
35
|
+
const detected = detectOrder.find(p => existsSync(p))
|
36
|
+
if (detected) config = detected
|
34
37
|
if (!existsSync(config)) {
|
35
38
|
console.log('⚙️ Config not found, creating default config...')
|
36
39
|
const templatePath = join(PKG_ROOT, 'templates', 'default.config.json')
|
package/lib/core/scanner.mjs
CHANGED
@@ -22,7 +22,12 @@ function parseArgs(argv) {
|
|
22
22
|
return args
|
23
23
|
}
|
24
24
|
|
25
|
-
function
|
25
|
+
function stripJsonComments(s) {
|
26
|
+
return String(s)
|
27
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
28
|
+
.replace(/(^|\s)\/\/.*$/gm, '') // line comments
|
29
|
+
}
|
30
|
+
function loadJson(p) { try { return JSON.parse(stripJsonComments(readFileSync(p, 'utf8'))) } catch { return null } }
|
26
31
|
|
27
32
|
async function listFiles(excludeDirs = []) {
|
28
33
|
const fg = (await req('fast-glob', 'fast-glob')).default
|
package/lib/core/scorer.mjs
CHANGED
@@ -25,7 +25,25 @@ 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
|
-
function
|
28
|
+
function stripJsonComments(s) {
|
29
|
+
return String(s)
|
30
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
31
|
+
.replace(/(^|\s)\/\/.*$/gm, '')
|
32
|
+
}
|
33
|
+
function loadConfig(pathFromArg) {
|
34
|
+
const fs = require('fs')
|
35
|
+
const paths = [pathFromArg, 'ai-test.config.jsonc', 'ai-test.config.json']
|
36
|
+
for (const p of paths) {
|
37
|
+
if (!p) continue
|
38
|
+
try {
|
39
|
+
if (fs.existsSync(p)) {
|
40
|
+
const raw = fs.readFileSync(p, 'utf8')
|
41
|
+
return JSON.parse(stripJsonComments(raw))
|
42
|
+
}
|
43
|
+
} catch {}
|
44
|
+
}
|
45
|
+
return {}
|
46
|
+
}
|
29
47
|
function pickWeight(cfg, key, def) { return cfg?.weights?.[key] ?? def }
|
30
48
|
function pickThreshold(cfg, key, def) { return cfg?.thresholds?.[key] ?? def }
|
31
49
|
|
@@ -276,14 +294,14 @@ function defaultMd(rows, statusMap = new Map()) {
|
|
276
294
|
let md = '<!-- UT Priority Scoring Report -->\n'
|
277
295
|
md += '<!-- Format: Status can be "TODO" | "DONE" | "SKIP" -->\n'
|
278
296
|
md += '<!-- You can mark status by replacing TODO with DONE or SKIP -->\n\n'
|
279
|
-
md += '| Status | Score | Priority | Name | Type | Layer | Path | Coverage | CS | BC | CC | ER | Testability | DepCount
|
280
|
-
md += '
|
297
|
+
md += '| Status | Score | Priority | Name | Type | Layer | Path | Coverage | CS | BC | CC | ER | Testability | DepCount |\n'
|
298
|
+
md += '|--------|-------|----------|------|------|-------|------|----------|----|----|----|----|-----------|----------|\n'
|
281
299
|
|
282
300
|
sorted.forEach(r => {
|
283
301
|
// 从状态映射中查找现有状态,否则默认为 TODO
|
284
302
|
const key = `${r.path}#${r.name}`
|
285
303
|
const status = statusMap.get(key) || 'TODO'
|
286
|
-
md += `| ${status} | ${r.score} | ${r.priority} | ${r.name} | ${r.type} | ${r.layerName || r.layer} | ${r.path} | ${typeof r.coveragePct === 'number' ? r.coveragePct.toFixed(1) + '%':'N/A'} | ${r.coverageScore ?? 'N/A'} | ${r.BC} | ${r.CC} | ${r.ER} | ${r.testability || r.ROI} | ${r.dependencyCount || 'N/A'}
|
304
|
+
md += `| ${status} | ${r.score} | ${r.priority} | ${r.name} | ${r.type} | ${r.layerName || r.layer} | ${r.path} | ${typeof r.coveragePct === 'number' ? r.coveragePct.toFixed(1) + '%':'N/A'} | ${r.coverageScore ?? 'N/A'} | ${r.BC} | ${r.CC} | ${r.ER} | ${r.testability || r.ROI} | ${r.dependencyCount || 'N/A'} |\n`
|
287
305
|
})
|
288
306
|
|
289
307
|
// 添加统计信息
|
@@ -315,7 +333,7 @@ function defaultCsv(rows) {
|
|
315
333
|
// 按总分降序排序(高分在前)
|
316
334
|
const sorted = [...rows].sort((a, b) => b.score - a.score)
|
317
335
|
|
318
|
-
const head = ['status','score','priority','name','path','type','layer','layerName','coveragePct','coverageScore','BC','CC','ER','testability','dependencyCount'
|
336
|
+
const head = ['status','score','priority','name','path','type','layer','layerName','coveragePct','coverageScore','BC','CC','ER','testability','dependencyCount'].join(',')
|
319
337
|
const body = sorted.map(r => [
|
320
338
|
'TODO',
|
321
339
|
r.score,
|
@@ -331,8 +349,7 @@ function defaultCsv(rows) {
|
|
331
349
|
r.CC,
|
332
350
|
r.ER,
|
333
351
|
r.testability || r.ROI,
|
334
|
-
r.dependencyCount || 'N/A'
|
335
|
-
`"${r.notes}"`
|
352
|
+
r.dependencyCount || 'N/A'
|
336
353
|
].join(',')).join('\n')
|
337
354
|
return head + '\n' + body + '\n'
|
338
355
|
}
|
@@ -583,28 +600,6 @@ async function main() {
|
|
583
600
|
}
|
584
601
|
}
|
585
602
|
|
586
|
-
// 构建 notes
|
587
|
-
const notesParts = []
|
588
|
-
const cognitive = eslintCognitive?.[`${path}#${name}`]
|
589
|
-
if (cognitive !== undefined) notesParts.push('CC:fused')
|
590
|
-
else notesParts.push('CC:cyclo-only')
|
591
|
-
if (internal && loc >= (cfg?.ccAdjust?.locBonusThreshold ?? 50)) notesParts.push(`CC+${cfg?.ccAdjust?.locBonus ?? 1}loc`)
|
592
|
-
if (CCFinal > CC) notesParts.push('CC+platform')
|
593
|
-
if (depGraphData && depGraphData.crossModuleScore > 0) notesParts.push(`ER+depGraph(${depGraphData.crossModuleScore})`)
|
594
|
-
if (!git) notesParts.push('ER:fallback')
|
595
|
-
else notesParts.push('ER:git')
|
596
|
-
if (overrides) {
|
597
|
-
const key = `${path}#${name}`
|
598
|
-
if (overrides.BC?.[key]) notesParts.push('BC:override')
|
599
|
-
if (overrides.CC?.[key]) notesParts.push('CC:override')
|
600
|
-
if (overrides.ER?.[key]) notesParts.push('ER:override')
|
601
|
-
if (overrides.ROI?.[key]) notesParts.push('ROI:override')
|
602
|
-
}
|
603
|
-
if (localImpact) notesParts.push('Impact:local')
|
604
|
-
if (localROI) notesParts.push('ROI:local')
|
605
|
-
notesParts.push(internal ? 'Internal:Y' : 'Internal:N')
|
606
|
-
if (!isMainChain(path, cfg)) notesParts.push(`BC:cap≤${cfg?.bcCapForNonMainChain ?? 8}`)
|
607
|
-
|
608
603
|
rows.push({
|
609
604
|
name,
|
610
605
|
path,
|
@@ -620,8 +615,7 @@ async function main() {
|
|
620
615
|
coveragePct: coveragePct ?? 'N/A',
|
621
616
|
coverageScore: coverageScore ?? 'N/A',
|
622
617
|
score,
|
623
|
-
priority
|
624
|
-
notes: notesParts.join('; ')
|
618
|
+
priority
|
625
619
|
})
|
626
620
|
}
|
627
621
|
|
package/package.json
CHANGED
@@ -0,0 +1,314 @@
|
|
1
|
+
{
|
2
|
+
// ========== 核心评分配置 ==========
|
3
|
+
"scoringMode": "layered", // 评分模式: "layered"(分层) 或 "legacy"(传统)
|
4
|
+
"round": {
|
5
|
+
"mode": "floor", // 取整模式: floor(向下) / ceil(向上) / round(四舍五入)
|
6
|
+
"digits": 2 // 保留小数位数
|
7
|
+
},
|
8
|
+
|
9
|
+
// ========== 扫描目标配置 ==========
|
10
|
+
"providers": ["func"], // 扫描目标类型: func(函数) / class(类) / method(方法)
|
11
|
+
"targetGeneration": {
|
12
|
+
"excludeDirs": [] // 排除的目录,例如: ["__tests__", "node_modules"]
|
13
|
+
},
|
14
|
+
"internalInclude": true, // 是否包含内部函数(非导出函数)
|
15
|
+
"internalThresholds": {
|
16
|
+
"minLoc": 15, // 内部函数最小行数阈值
|
17
|
+
"bonusLoc": 50, // 超过此行数给予额外关注
|
18
|
+
"excludePatterns": [ // 排除的文件模式
|
19
|
+
"**/__tests__/**",
|
20
|
+
"**/*.spec.ts",
|
21
|
+
"**/*.spec.tsx"
|
22
|
+
]
|
23
|
+
},
|
24
|
+
|
25
|
+
// ========== 分层架构配置 ==========
|
26
|
+
// 测试金字塔:Foundation(底层) -> Business Logic -> State Management -> UI Components(顶层)
|
27
|
+
"layers": {
|
28
|
+
// 基础工具层:纯函数、工具函数、无依赖的基础代码
|
29
|
+
"foundation": {
|
30
|
+
"name": "Foundation (基础工具层)",
|
31
|
+
"description": "纯函数、工具函数、无依赖的基础代码",
|
32
|
+
"patterns": ["utils/**", "constants/**", "config/**", "types/**"],
|
33
|
+
"characteristics": {
|
34
|
+
"isPure": true, // 是否为纯函数
|
35
|
+
"noDependencies": true, // 是否无依赖
|
36
|
+
"multipleReferences": true // 是否被多处引用
|
37
|
+
},
|
38
|
+
"weights": {
|
39
|
+
"testability": 0.45, // 可测试性权重(最高)
|
40
|
+
"dependencyCount": 0.25, // 依赖数量权重
|
41
|
+
"complexity": 0.20, // 复杂度权重
|
42
|
+
"coverage": 0.10 // 覆盖率权重
|
43
|
+
},
|
44
|
+
"thresholds": { "P0": 7.5, "P1": 6.0, "P2": 4.0 },
|
45
|
+
"coverageTarget": 100, // 目标覆盖率 100%
|
46
|
+
"autoP0": false
|
47
|
+
},
|
48
|
+
|
49
|
+
// 业务逻辑层:包含业务规则的函数,不包含UI
|
50
|
+
"business": {
|
51
|
+
"name": "Business Logic (业务逻辑层)",
|
52
|
+
"description": "包含业务规则的函数,不包含UI",
|
53
|
+
"patterns": ["services/**", "stores/**", "hooks/**"],
|
54
|
+
"characteristics": {
|
55
|
+
"hasBusinessRules": true,
|
56
|
+
"noUI": true,
|
57
|
+
"mayDependOnUtils": true
|
58
|
+
},
|
59
|
+
"weights": {
|
60
|
+
"businessCriticality": 0.35, // 业务关键度权重(最高)
|
61
|
+
"complexity": 0.25,
|
62
|
+
"errorRisk": 0.25,
|
63
|
+
"coverage": 0.15
|
64
|
+
},
|
65
|
+
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
66
|
+
"coverageTarget": 80
|
67
|
+
},
|
68
|
+
|
69
|
+
// 状态管理层:Jotai atoms、Zustand stores等
|
70
|
+
"state": {
|
71
|
+
"name": "State Management (状态管理层)",
|
72
|
+
"description": "Jotai atoms、Zustand stores等状态管理",
|
73
|
+
"patterns": ["atoms/**", "stores/**"],
|
74
|
+
"characteristics": {
|
75
|
+
"isStateManagement": true,
|
76
|
+
"connectsLogicAndUI": true
|
77
|
+
},
|
78
|
+
"weights": {
|
79
|
+
"businessCriticality": 0.45,
|
80
|
+
"complexity": 0.25,
|
81
|
+
"errorRisk": 0.20,
|
82
|
+
"coverage": 0.10
|
83
|
+
},
|
84
|
+
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
85
|
+
"coverageTarget": 70
|
86
|
+
},
|
87
|
+
|
88
|
+
// UI组件层:React组件、包含交互逻辑
|
89
|
+
"ui": {
|
90
|
+
"name": "UI Components (UI组件层)",
|
91
|
+
"description": "React组件、包含交互逻辑",
|
92
|
+
"patterns": ["components/**", "pages/**", "context/**"],
|
93
|
+
"characteristics": {
|
94
|
+
"isReactComponent": true,
|
95
|
+
"hasInteraction": true
|
96
|
+
},
|
97
|
+
"weights": {
|
98
|
+
"businessCriticality": 0.35,
|
99
|
+
"complexity": 0.25,
|
100
|
+
"testability": 0.25,
|
101
|
+
"coverage": 0.15
|
102
|
+
},
|
103
|
+
"thresholds": { "P0": 8.5, "P1": 7.0, "P2": 5.0 },
|
104
|
+
"coverageTarget": 50 // UI层目标覆盖率适当降低
|
105
|
+
}
|
106
|
+
},
|
107
|
+
|
108
|
+
// ========== 覆盖率评分配置 ==========
|
109
|
+
// 参考: Meta TestGen-LLM 覆盖率增量验证
|
110
|
+
"coverageScoring": {
|
111
|
+
"naScore": 5, // 无覆盖率数据时的默认分数
|
112
|
+
"mapping": [ // 覆盖率百分比到分数的映射(覆盖率越低,分数越高,优先级越高)
|
113
|
+
{ "lte": 0, "score": 10 }, // 0% 覆盖率 -> 最高优先级
|
114
|
+
{ "lte": 40, "score": 8 }, // ≤40% -> 高优先级
|
115
|
+
{ "lte": 70, "score": 6 }, // ≤70% -> 中等优先级
|
116
|
+
{ "lte": 90, "score": 3 }, // ≤90% -> 低优先级
|
117
|
+
{ "lte": 100, "score": 1 } // 100% 覆盖率 -> 最低优先级
|
118
|
+
]
|
119
|
+
},
|
120
|
+
|
121
|
+
// ========== 覆盖率自动扫描配置 ==========
|
122
|
+
"coverage": {
|
123
|
+
"runBeforeScan": true, // 是否在扫描前自动运行覆盖率分析
|
124
|
+
"command": "npx jest --coverage --silent" // 覆盖率分析命令
|
125
|
+
},
|
126
|
+
|
127
|
+
// ========== 传统评分模式的权重配置 ==========
|
128
|
+
// 仅在 scoringMode: "legacy" 时使用
|
129
|
+
"weights": {
|
130
|
+
"BC": 0.25, // Business Criticality (业务关键度)
|
131
|
+
"CC": 0.15, // Cyclomatic Complexity (圈复杂度)
|
132
|
+
"ER": 0.15, // Error Risk (错误风险)
|
133
|
+
"ROI": 0.15, // Return on Investment (投资回报率,即可测试性)
|
134
|
+
"dependencyCount": 0.10, // 依赖数量
|
135
|
+
"coverage": 0.20 // 覆盖率权重(新增,参考 CodeScene 的热点分析)
|
136
|
+
},
|
137
|
+
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
138
|
+
|
139
|
+
// ========== 覆盖率加成配置 ==========
|
140
|
+
// 参考: ISTQB 风险驱动测试
|
141
|
+
"coverageBoost": {
|
142
|
+
"enable": true,
|
143
|
+
"threshold": 60, // 覆盖率低于此值时启用加成
|
144
|
+
"scale": 0.5, // 加成系数
|
145
|
+
"maxBoost": 0.5 // 最大加成值
|
146
|
+
},
|
147
|
+
|
148
|
+
// ========== 复杂度融合配置 ==========
|
149
|
+
// 圈复杂度 + 认知复杂度融合
|
150
|
+
"ccFusion": {
|
151
|
+
"useCognitive": true, // 是否启用认知复杂度
|
152
|
+
"cyclomaticT": 15, // 圈复杂度阈值
|
153
|
+
"cognitiveT": 25, // 认知复杂度阈值
|
154
|
+
"wC": 0.7, // 圈复杂度权重
|
155
|
+
"wK": 0.3, // 认知复杂度权重
|
156
|
+
"cap": 10 // 最大分数上限
|
157
|
+
},
|
158
|
+
|
159
|
+
// ========== 复杂度调整配置 ==========
|
160
|
+
"ccAdjust": {
|
161
|
+
"depthW": 0.2, // 深度权重
|
162
|
+
"branchesW": 0.2, // 分支数权重
|
163
|
+
"paramsW": 0.1, // 参数数量权重
|
164
|
+
"locBonusThreshold": 50, // 行数加成阈值
|
165
|
+
"locBonus": 1 // 行数加成分数
|
166
|
+
},
|
167
|
+
|
168
|
+
// ========== 业务关键度关键词配置 ==========
|
169
|
+
// 参考: ISTQB 风险驱动测试 - API表面暴露度
|
170
|
+
"bcKeywords": {
|
171
|
+
"10": ["price", "booking", "payment", "checkout"], // 支付核心
|
172
|
+
"9": ["recommend", "search"], // 搜索推荐
|
173
|
+
"8": ["filter", "list"], // 列表筛选
|
174
|
+
"7": ["login", "order", "config"], // 登录配置
|
175
|
+
"5": ["navigation", "seo", "header"], // 导航SEO
|
176
|
+
"3": ["log", "trace", "decor"] // 日志装饰
|
177
|
+
},
|
178
|
+
|
179
|
+
// ========== 主链路配置 ==========
|
180
|
+
"mainChainPaths": [], // 主链路路径,例如: ["src/services/booking/**"]
|
181
|
+
"bcCapForNonMainChain": 8, // 非主链路业务关键度上限
|
182
|
+
|
183
|
+
// ========== 圈复杂度映射配置 ==========
|
184
|
+
"ccMapping": {
|
185
|
+
"cyclomatic": [
|
186
|
+
{ "gt": 15, "score": 10 }, // >15 -> 最复杂
|
187
|
+
{ "gte": 11, "lte": 15, "score": 10 }, // 11-15 -> 高复杂
|
188
|
+
{ "gte": 6, "lte": 10, "score": 9 }, // 6-10 -> 中等复杂
|
189
|
+
{ "gte": 3, "lte": 5, "score": 7 }, // 3-5 -> 低复杂
|
190
|
+
{ "lte": 2, "score": 6 } // ≤2 -> 简单
|
191
|
+
],
|
192
|
+
"adjustments": [ // 额外复杂度调整规则
|
193
|
+
{ "field": "maxDepth", "op": ">=", "value": 4, "delta": 1 }, // 深度≥4 +1分
|
194
|
+
{ "field": "branches", "op": ">=", "value": 12, "delta": 1 }, // 分支≥12 +1分
|
195
|
+
{ "field": "params", "op": ">=", "value": 6, "delta": 1 }, // 参数≥6 +1分
|
196
|
+
{ "field": "statements", "op": ">=", "value": 80, "delta": 1 }, // 语句≥80 +1分
|
197
|
+
{ "field": "cognitive", "op": ">=", "value": 25, "delta": 1 } // 认知≥25 +1分
|
198
|
+
],
|
199
|
+
"cap": 10,
|
200
|
+
"platformAdjust": {
|
201
|
+
"delta": 1, // 平台相关代码额外+1分
|
202
|
+
"cap": 10,
|
203
|
+
"skipIfLikelihoodGte": 4 // 如果错误可能性≥4则跳过此调整
|
204
|
+
}
|
205
|
+
},
|
206
|
+
|
207
|
+
// ========== 降级复杂度映射配置 ==========
|
208
|
+
"fallbackMapping": {
|
209
|
+
"conditions": [
|
210
|
+
{ "gt": 12, "score": 10 },
|
211
|
+
{ "gte": 8, "lte": 12, "score": 8 },
|
212
|
+
{ "gte": 5, "lte": 7, "score": 6 },
|
213
|
+
{ "gte": 3, "lte": 4, "score": 4 },
|
214
|
+
{ "lte": 2, "score": 2 }
|
215
|
+
],
|
216
|
+
"nesting": [
|
217
|
+
{ "gte": 4, "delta": 2 },
|
218
|
+
{ "eq": 3, "delta": 1 }
|
219
|
+
],
|
220
|
+
"earlyReturns": [
|
221
|
+
{ "gte": 4, "delta": 1 }
|
222
|
+
],
|
223
|
+
"paramsOrSources": [
|
224
|
+
{ "paramsGte": 6, "delta": 1 },
|
225
|
+
{ "sourcesGte": 3, "delta": 1 }
|
226
|
+
],
|
227
|
+
"cap": 10
|
228
|
+
},
|
229
|
+
|
230
|
+
// ========== 依赖图配置 ==========
|
231
|
+
"depGraph": {
|
232
|
+
"enable": true,
|
233
|
+
"neighborCategoryBoost": 2, // 邻近类别加成
|
234
|
+
"degreeBoost": 8 // 依赖度加成
|
235
|
+
},
|
236
|
+
|
237
|
+
// ========== 错误风险矩阵配置 ==========
|
238
|
+
// 参考: Meta TestGen-LLM 稳定性重跑验证
|
239
|
+
// ER = 错误可能性 (Likelihood) × 影响度 (Impact)
|
240
|
+
"erMatrix": {
|
241
|
+
"5": { "5": 10, "4": 9, "3": 8, "2": 7, "1": 6 }, // 可能性=5
|
242
|
+
"4": { "5": 9, "4": 8, "3": 7, "2": 6, "1": 5 }, // 可能性=4
|
243
|
+
"3": { "5": 8, "4": 7, "3": 6, "2": 5, "1": 4 }, // 可能性=3
|
244
|
+
"2": { "5": 7, "4": 6, "3": 5, "2": 4, "1": 3 }, // 可能性=2
|
245
|
+
"1": { "5": 6, "4": 5, "3": 4, "2": 3, "1": 2 } // 可能性=1
|
246
|
+
},
|
247
|
+
|
248
|
+
// ========== 错误可能性规则配置 ==========
|
249
|
+
// 参考: CodeScene 的变更频率(Churn)分析
|
250
|
+
"likelihoodRules": [
|
251
|
+
{ "field": "commits30d", "op": ">=", "value": 6, "score": 5 }, // 30天内≥6次提交 -> 高频变更
|
252
|
+
{ "field": "commits30d", "op": "between", "min": 3, "max": 5, "score": 4 },
|
253
|
+
{ "field": "commits30d", "op": "between", "min": 1, "max": 2, "score": 3 },
|
254
|
+
{ "field": "fallback90d", "op": "gt", "value": 0, "score": 2 }, // 90天内有变更
|
255
|
+
{ "field": "fallback180dZero", "op": "eq", "value": true, "score": 1 } // 180天内无变更
|
256
|
+
],
|
257
|
+
|
258
|
+
// ========== 加成规则配置 ==========
|
259
|
+
"boostRules": {
|
260
|
+
"authors30dGte": 3, // 30天内贡献者≥3人
|
261
|
+
"crossModule": true, // 跨模块引用
|
262
|
+
"multiPlatform": true, // 多平台代码
|
263
|
+
"cap": 5 // 加成上限
|
264
|
+
},
|
265
|
+
|
266
|
+
// ========== 跨模块类别配置 ==========
|
267
|
+
"crossModuleCategories": ["components", "hooks", "utils", "services", "pages"],
|
268
|
+
|
269
|
+
// ========== 本地提示映射配置 ==========
|
270
|
+
"hintMaps": {
|
271
|
+
"impactLocal": "configs/impact.local.json",
|
272
|
+
"roiLocal": "configs/roi.local.json"
|
273
|
+
},
|
274
|
+
|
275
|
+
// ========== 覆盖配置 ==========
|
276
|
+
"overrides": "reports/overrides.json",
|
277
|
+
|
278
|
+
// ========== 影响度关键词配置 ==========
|
279
|
+
"impactKeywords": {
|
280
|
+
"5": ["payment", "booking", "price"],
|
281
|
+
"4": ["filter", "list", "display"],
|
282
|
+
"3": ["interaction"],
|
283
|
+
"2": ["minor", "ui"],
|
284
|
+
"1": ["decor", "cosmetic"]
|
285
|
+
},
|
286
|
+
|
287
|
+
// ========== 可测试性规则配置 ==========
|
288
|
+
// 纯函数和可注入函数最适合AI生成测试
|
289
|
+
"testabilityRules": {
|
290
|
+
"pure": 10, // 纯函数 -> 最易测试
|
291
|
+
"injectable": 9, // 依赖注入 -> 很易测试
|
292
|
+
"multiContext": 7, // 多上下文 -> 中等难度
|
293
|
+
"nativeOrNetwork": 5, // 原生API/网络 -> 需要Mock
|
294
|
+
"needsUI": 3 // 需要UI环境 -> 较难测试
|
295
|
+
},
|
296
|
+
|
297
|
+
// ========== 依赖数量映射配置 ==========
|
298
|
+
// 依赖越多,集成风险越高,测试价值越高
|
299
|
+
"dependencyCountMapping": [
|
300
|
+
{ "gte": 10, "score": 10 }, // ≥10个依赖 -> 高集成风险
|
301
|
+
{ "gte": 5, "lt": 10, "score": 10 },
|
302
|
+
{ "gte": 3, "lt": 5, "score": 9 },
|
303
|
+
{ "gte": 1, "lt": 3, "score": 7 },
|
304
|
+
{ "eq": 0, "score": 5 } // 0个依赖 -> 纯函数(低风险但高可测试性)
|
305
|
+
],
|
306
|
+
|
307
|
+
// ========== 降级默认值配置 ==========
|
308
|
+
"fallbacks": {
|
309
|
+
"BC": 6, // 业务关键度默认值
|
310
|
+
"CC": 6, // 复杂度默认值
|
311
|
+
"ERLikelihood": 3, // 错误可能性默认值
|
312
|
+
"Testability": 6 // 可测试性默认值
|
313
|
+
}
|
314
|
+
}
|
package/lib/extract-tests.mjs
DELETED
@@ -1,199 +0,0 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
/**
|
3
|
-
* 从 AI 回复中提取测试文件并自动创建
|
4
|
-
*/
|
5
|
-
|
6
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
7
|
-
import { dirname } from 'path';
|
8
|
-
|
9
|
-
/**
|
10
|
-
* 从 AI 响应中提取测试文件
|
11
|
-
*/
|
12
|
-
export function extractTests(content, options = {}) {
|
13
|
-
const { overwrite = false, dryRun = false } = options;
|
14
|
-
|
15
|
-
// 先尝试解析 JSON Manifest(优先)
|
16
|
-
// 形如:```json { version: 1, files: [{ path, source, ... }] }
|
17
|
-
let manifest = null;
|
18
|
-
const manifestRegex = /```json\s*\n([\s\S]*?)\n```/gi;
|
19
|
-
let m;
|
20
|
-
while ((m = manifestRegex.exec(content)) !== null) {
|
21
|
-
const jsonStr = m[1].trim();
|
22
|
-
try {
|
23
|
-
const obj = JSON.parse(jsonStr);
|
24
|
-
if (obj && Array.isArray(obj.files)) {
|
25
|
-
manifest = obj;
|
26
|
-
break;
|
27
|
-
}
|
28
|
-
} catch {}
|
29
|
-
}
|
30
|
-
const manifestPaths = manifest?.files?.map(f => String(f.path).trim()) ?? [];
|
31
|
-
|
32
|
-
// 再匹配多种文件代码块格式(回退/兼容,增强鲁棒性)
|
33
|
-
const patterns = [
|
34
|
-
// 格式1: ### 测试文件: path (有/无反引号)
|
35
|
-
/###\s*测试文件\s*[::]\s*`?([^\n`]+?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
36
|
-
// 格式2: **测试文件**: path
|
37
|
-
/\*\*测试文件\*\*\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
38
|
-
// 格式3: 文件路径: path
|
39
|
-
/文件路径\s*[::]\s*`?([^\n`]+)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx|json|text)?\s*\n([\s\S]*?)\n```/gi,
|
40
|
-
// 格式4: # path.test.ts (仅文件名标题)
|
41
|
-
/^#+\s+([^\n]+\.test\.[jt]sx?)\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gim,
|
42
|
-
// 格式5: 更宽松的匹配(任意"path"后接代码块,路径必须含 .test.)
|
43
|
-
/(?:path|文件|file)\s*[::]?\s*`?([^\n`]*\.test\.[jt]sx?[^\n`]*?)`?\s*\n```(?:typescript|ts|tsx|javascript|js|jsx)?\s*\n([\s\S]*?)\n```/gi,
|
44
|
-
];
|
45
|
-
|
46
|
-
const created = [];
|
47
|
-
const skipped = [];
|
48
|
-
const errors = [];
|
49
|
-
|
50
|
-
patterns.forEach(fileRegex => {
|
51
|
-
let match;
|
52
|
-
while ((match = fileRegex.exec(content)) !== null) {
|
53
|
-
const [, filePath, testCode] = match;
|
54
|
-
const cleanPath = filePath.trim();
|
55
|
-
|
56
|
-
// 若存在 Manifest,则只允许清单内的路径
|
57
|
-
if (manifestPaths.length > 0 && !manifestPaths.includes(cleanPath)) {
|
58
|
-
continue;
|
59
|
-
}
|
60
|
-
|
61
|
-
// 跳过已处理的文件
|
62
|
-
if (created.some(f => f.path === cleanPath) ||
|
63
|
-
skipped.some(f => f.path === cleanPath)) {
|
64
|
-
continue;
|
65
|
-
}
|
66
|
-
|
67
|
-
// 检查文件是否已存在
|
68
|
-
if (existsSync(cleanPath) && !overwrite) {
|
69
|
-
skipped.push({ path: cleanPath, reason: 'exists' });
|
70
|
-
continue;
|
71
|
-
}
|
72
|
-
|
73
|
-
if (dryRun) {
|
74
|
-
created.push({ path: cleanPath, code: testCode.trim(), dryRun: true });
|
75
|
-
continue;
|
76
|
-
}
|
77
|
-
|
78
|
-
try {
|
79
|
-
// 确保目录存在
|
80
|
-
mkdirSync(dirname(cleanPath), { recursive: true });
|
81
|
-
|
82
|
-
// 写入测试文件
|
83
|
-
const cleanCode = testCode.trim();
|
84
|
-
writeFileSync(cleanPath, cleanCode + '\n');
|
85
|
-
created.push({ path: cleanPath, code: cleanCode });
|
86
|
-
} catch (err) {
|
87
|
-
errors.push({ path: cleanPath, error: err.message });
|
88
|
-
}
|
89
|
-
}
|
90
|
-
});
|
91
|
-
|
92
|
-
// 若有 Manifest 但未生成任何文件,则报告可能缺失代码块
|
93
|
-
if (manifestPaths.length > 0) {
|
94
|
-
const createdPaths = new Set(created.map(f => f.path));
|
95
|
-
const missing = manifestPaths.filter(p => !createdPaths.has(p));
|
96
|
-
missing.forEach(p => errors.push({ path: p, error: 'missing code block for manifest entry' }));
|
97
|
-
}
|
98
|
-
|
99
|
-
return { created, skipped, errors };
|
100
|
-
}
|
101
|
-
|
102
|
-
/**
|
103
|
-
* CLI 入口
|
104
|
-
*/
|
105
|
-
export function runCLI(argv = process.argv) {
|
106
|
-
const args = argv.slice(2);
|
107
|
-
|
108
|
-
if (args.length === 0) {
|
109
|
-
console.error('❌ 缺少参数\n');
|
110
|
-
console.error('用法: ai-test extract-tests <response-file> [options]\n');
|
111
|
-
console.error('选项:');
|
112
|
-
console.error(' --overwrite 覆盖已存在的文件');
|
113
|
-
console.error(' --dry-run 仅显示将要创建的文件,不实际写入\n');
|
114
|
-
console.error('示例:');
|
115
|
-
console.error(' ai-test extract-tests response.txt');
|
116
|
-
console.error(' ai-test extract-tests response.txt --overwrite');
|
117
|
-
process.exit(1);
|
118
|
-
}
|
119
|
-
|
120
|
-
const responseFile = args[0];
|
121
|
-
const options = {
|
122
|
-
overwrite: args.includes('--overwrite'),
|
123
|
-
dryRun: args.includes('--dry-run')
|
124
|
-
};
|
125
|
-
|
126
|
-
if (!existsSync(responseFile)) {
|
127
|
-
console.error(`❌ 文件不存在: ${responseFile}`);
|
128
|
-
process.exit(1);
|
129
|
-
}
|
130
|
-
|
131
|
-
try {
|
132
|
-
const content = readFileSync(responseFile, 'utf8');
|
133
|
-
const { created, skipped, errors } = extractTests(content, options);
|
134
|
-
|
135
|
-
// 输出结果
|
136
|
-
created.forEach(f => {
|
137
|
-
if (f.dryRun) {
|
138
|
-
console.log(`[DRY-RUN] 将创建: ${f.path}`);
|
139
|
-
} else {
|
140
|
-
console.log(`✅ 创建测试文件: ${f.path}`);
|
141
|
-
}
|
142
|
-
});
|
143
|
-
|
144
|
-
skipped.forEach(f => {
|
145
|
-
if (f.reason === 'exists') {
|
146
|
-
console.log(`⚠️ 跳过(已存在): ${f.path}`);
|
147
|
-
}
|
148
|
-
});
|
149
|
-
|
150
|
-
errors.forEach(f => {
|
151
|
-
console.error(`❌ 创建失败: ${f.path} - ${f.error}`);
|
152
|
-
});
|
153
|
-
|
154
|
-
console.log('');
|
155
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
156
|
-
console.log(`✨ 总共创建 ${created.length} 个测试文件`);
|
157
|
-
if (skipped.length > 0) {
|
158
|
-
console.log(`⚠️ 跳过 ${skipped.length} 个已存在的文件`);
|
159
|
-
}
|
160
|
-
if (errors.length > 0) {
|
161
|
-
console.log(`❌ ${errors.length} 个文件创建失败`);
|
162
|
-
}
|
163
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
164
|
-
console.log('');
|
165
|
-
|
166
|
-
if (created.length > 0 && !options.dryRun) {
|
167
|
-
console.log('🧪 运行测试:');
|
168
|
-
console.log(' npm test');
|
169
|
-
console.log('');
|
170
|
-
console.log('📝 标记完成(如需):');
|
171
|
-
const functionNames = created
|
172
|
-
.map(f => f.path.match(/\/([^/]+)\.test\./)?.[1])
|
173
|
-
.filter(Boolean)
|
174
|
-
.join(',');
|
175
|
-
if (functionNames) {
|
176
|
-
console.log(` ai-test mark-done "${functionNames}"`);
|
177
|
-
}
|
178
|
-
} else if (created.length === 0) {
|
179
|
-
console.log('❌ 未找到任何测试文件');
|
180
|
-
console.log('');
|
181
|
-
console.log('请检查 AI 回复格式是否正确。预期格式:');
|
182
|
-
console.log(' ### 测试文件: src/utils/xxx.test.ts');
|
183
|
-
console.log(' ```typescript');
|
184
|
-
console.log(' // 测试代码');
|
185
|
-
console.log(' ```');
|
186
|
-
}
|
187
|
-
|
188
|
-
process.exit(errors.length > 0 ? 1 : 0);
|
189
|
-
} catch (err) {
|
190
|
-
console.error(`❌ 错误: ${err.message}`);
|
191
|
-
process.exit(1);
|
192
|
-
}
|
193
|
-
}
|
194
|
-
|
195
|
-
// 作为脚本直接运行时
|
196
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
197
|
-
runCLI();
|
198
|
-
}
|
199
|
-
|
package/lib/index.js
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* @ctrip/ut-priority-scorer
|
3
|
-
*
|
4
|
-
* AI-friendly Unit Test priority scoring system
|
5
|
-
* Based on layered architecture and testability metrics
|
6
|
-
*/
|
7
|
-
|
8
|
-
export { default as genTargets } from './gen-targets.mjs'
|
9
|
-
export { default as genGitSignals } from './gen-git-signals.mjs'
|
10
|
-
export { default as score } from './score-ut.mjs'
|
11
|
-
|
12
|
-
// 版本信息
|
13
|
-
export const version = '1.0.0'
|
14
|
-
|
15
|
-
// 默认配置路径
|
16
|
-
export const DEFAULT_CONFIG_PATH = 'ut_scoring_config.json'
|
17
|
-
export const DEFAULT_OUTPUT_DIR = 'reports'
|
18
|
-
|
package/lib/run-batch.mjs
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
#!/usr/bin/env node
|
2
|
-
/**
|
3
|
-
* 单批次:生成 prompt → 调用 AI → 提取测试 → 运行 Jest → 输出失败摘要
|
4
|
-
*/
|
5
|
-
|
6
|
-
import { spawn } from 'child_process'
|
7
|
-
import { existsSync, readFileSync } from 'fs'
|
8
|
-
|
9
|
-
function sh(cmd, args = []) {
|
10
|
-
return new Promise((resolve, reject) => {
|
11
|
-
const child = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd() })
|
12
|
-
child.on('close', code => code === 0 ? resolve(0) : reject(new Error(`${cmd} exited ${code}`)))
|
13
|
-
child.on('error', reject)
|
14
|
-
})
|
15
|
-
}
|
16
|
-
|
17
|
-
function readCoverageSummary() {
|
18
|
-
const path = 'coverage/coverage-summary.json'
|
19
|
-
if (!existsSync(path)) return null
|
20
|
-
try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }
|
21
|
-
}
|
22
|
-
|
23
|
-
function getCoveragePercent(summary) {
|
24
|
-
if (!summary || !summary.total) return 0
|
25
|
-
return summary.total.lines?.pct ?? 0
|
26
|
-
}
|
27
|
-
|
28
|
-
async function main(argv = process.argv) {
|
29
|
-
const args = argv.slice(2)
|
30
|
-
const priority = args[0] || 'P0'
|
31
|
-
const limit = Number(args[1] || 10)
|
32
|
-
const skip = Number(args[2] || 0)
|
33
|
-
const minCovDelta = priority === 'P0' ? 2 : (priority === 'P1' ? 1 : 0)
|
34
|
-
|
35
|
-
// 记录初始覆盖率
|
36
|
-
const beforeCov = getCoveragePercent(readCoverageSummary())
|
37
|
-
|
38
|
-
// 1) 生成 Prompt(加入上一轮失败提示 hints.txt 如存在)
|
39
|
-
const promptArgs = ['ai-test-generator/lib/gen-test-prompt.mjs', '--report', 'reports/ut_scores.md', '-p', priority, '-n', String(limit), '--skip', String(skip)]
|
40
|
-
try { await sh('node', [...promptArgs, '--hints-file', 'reports/hints.txt']) } catch { await sh('node', promptArgs) }
|
41
|
-
|
42
|
-
// 2) 调用 AI
|
43
|
-
await sh('node', ['ai-test-generator/lib/ai-generate.mjs', '--prompt', 'prompt.txt', '--out', 'reports/ai_response.txt'])
|
44
|
-
|
45
|
-
// 3) 提取测试
|
46
|
-
await sh('node', ['ai-test-generator/lib/extract-tests.mjs', 'reports/ai_response.txt', '--overwrite'])
|
47
|
-
|
48
|
-
// 4) 运行 Jest(按优先级自适应重跑与覆盖率增量)
|
49
|
-
const reruns = priority === 'P0' ? 1 : (priority === 'P1' ? 0 : 0)
|
50
|
-
for (let i = 0; i < Math.max(1, reruns + 1); i++) {
|
51
|
-
try { await sh('node', ['ai-test-generator/lib/jest-runner.mjs']) } catch { /* 继续做失败分析 */ }
|
52
|
-
}
|
53
|
-
|
54
|
-
// 校验覆盖率增量(P0/P1)
|
55
|
-
if (minCovDelta > 0) {
|
56
|
-
const afterCov = getCoveragePercent(readCoverageSummary())
|
57
|
-
const delta = afterCov - beforeCov
|
58
|
-
if (delta < minCovDelta) {
|
59
|
-
console.warn(`⚠️ Coverage delta ${delta.toFixed(2)}% < required ${minCovDelta}% (before: ${beforeCov.toFixed(2)}%, after: ${afterCov.toFixed(2)}%)`)
|
60
|
-
} else {
|
61
|
-
console.log(`✅ Coverage improved: ${beforeCov.toFixed(2)}% → ${afterCov.toFixed(2)}% (+${delta.toFixed(2)}%)`)
|
62
|
-
}
|
63
|
-
}
|
64
|
-
|
65
|
-
// 5) 失败分析并落盘 hints
|
66
|
-
const { spawn } = await import('child_process')
|
67
|
-
const { writeFileSync } = await import('fs')
|
68
|
-
await new Promise((resolve) => {
|
69
|
-
const child = spawn('node', ['ai-test-generator/lib/jest-failure-analyzer.mjs'], { stdio: ['inherit','pipe','inherit'] })
|
70
|
-
const chunks = []
|
71
|
-
child.stdout.on('data', d => chunks.push(Buffer.from(d)))
|
72
|
-
child.on('close', () => {
|
73
|
-
try { const obj = JSON.parse(Buffer.concat(chunks).toString('utf8')); writeFileSync('reports/hints.txt', obj.hints?.length ? `# 上一轮失败修复建议\n- ${obj.hints.join('\n- ')}` : '') } catch {}
|
74
|
-
resolve()
|
75
|
-
})
|
76
|
-
})
|
77
|
-
}
|
78
|
-
|
79
|
-
if (import.meta.url === `file://${process.argv[1]}`) main()
|
80
|
-
|
81
|
-
|
@@ -1,227 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"scoringMode": "layered",
|
3
|
-
"round": { "mode": "floor", "digits": 2 },
|
4
|
-
"providers": ["func"],
|
5
|
-
"targetGeneration": {
|
6
|
-
"excludeDirs": []
|
7
|
-
},
|
8
|
-
"internalInclude": true,
|
9
|
-
"internalThresholds": {
|
10
|
-
"minLoc": 15,
|
11
|
-
"bonusLoc": 50,
|
12
|
-
"excludePatterns": ["**/__tests__/**", "**/*.spec.ts", "**/*.spec.tsx"]
|
13
|
-
},
|
14
|
-
"layers": {
|
15
|
-
"foundation": {
|
16
|
-
"name": "Foundation (基础工具层)",
|
17
|
-
"description": "纯函数、工具函数、无依赖的基础代码",
|
18
|
-
"patterns": ["utils/**", "constants/**", "config/**", "types/**"],
|
19
|
-
"characteristics": {
|
20
|
-
"isPure": true,
|
21
|
-
"noDependencies": true,
|
22
|
-
"multipleReferences": true
|
23
|
-
},
|
24
|
-
"weights": {
|
25
|
-
"testability": 0.45,
|
26
|
-
"dependencyCount": 0.25,
|
27
|
-
"complexity": 0.20,
|
28
|
-
"coverage": 0.10
|
29
|
-
},
|
30
|
-
"thresholds": { "P0": 7.5, "P1": 6.0, "P2": 4.0 },
|
31
|
-
"coverageTarget": 100,
|
32
|
-
"autoP0": false,
|
33
|
-
"notes": "测试金字塔底层,优先级最高;AI生成友好层"
|
34
|
-
},
|
35
|
-
"business": {
|
36
|
-
"name": "Business Logic (业务逻辑层)",
|
37
|
-
"description": "包含业务规则的函数,不包含UI",
|
38
|
-
"patterns": ["services/**", "stores/**", "hooks/**"],
|
39
|
-
"characteristics": {
|
40
|
-
"hasBusinessRules": true,
|
41
|
-
"noUI": true,
|
42
|
-
"mayDependOnUtils": true
|
43
|
-
},
|
44
|
-
"weights": {
|
45
|
-
"businessCriticality": 0.35,
|
46
|
-
"complexity": 0.25,
|
47
|
-
"errorRisk": 0.25,
|
48
|
-
"coverage": 0.15
|
49
|
-
},
|
50
|
-
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
51
|
-
"coverageTarget": 80,
|
52
|
-
"notes": "核心业务逻辑,高优先级"
|
53
|
-
},
|
54
|
-
"state": {
|
55
|
-
"name": "State Management (状态管理层)",
|
56
|
-
"description": "Jotai atoms、Zustand stores等状态管理",
|
57
|
-
"patterns": ["atoms/**", "stores/**"],
|
58
|
-
"characteristics": {
|
59
|
-
"isStateManagement": true,
|
60
|
-
"connectsLogicAndUI": true
|
61
|
-
},
|
62
|
-
"weights": {
|
63
|
-
"businessCriticality": 0.45,
|
64
|
-
"complexity": 0.25,
|
65
|
-
"errorRisk": 0.20,
|
66
|
-
"coverage": 0.10
|
67
|
-
},
|
68
|
-
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
69
|
-
"coverageTarget": 70,
|
70
|
-
"notes": "状态管理,连接逻辑和UI"
|
71
|
-
},
|
72
|
-
"ui": {
|
73
|
-
"name": "UI Components (UI组件层)",
|
74
|
-
"description": "React组件、包含交互逻辑",
|
75
|
-
"patterns": ["components/**", "pages/**", "context/**"],
|
76
|
-
"characteristics": {
|
77
|
-
"isReactComponent": true,
|
78
|
-
"hasInteraction": true
|
79
|
-
},
|
80
|
-
"weights": {
|
81
|
-
"businessCriticality": 0.35,
|
82
|
-
"complexity": 0.25,
|
83
|
-
"testability": 0.25,
|
84
|
-
"coverage": 0.15
|
85
|
-
},
|
86
|
-
"thresholds": { "P0": 8.5, "P1": 7.0, "P2": 5.0 },
|
87
|
-
"coverageTarget": 50,
|
88
|
-
"notes": "UI层,更适合集成测试"
|
89
|
-
}
|
90
|
-
},
|
91
|
-
"coverageScoring": {
|
92
|
-
"naScore": 5,
|
93
|
-
"mapping": [
|
94
|
-
{ "lte": 0, "score": 10 },
|
95
|
-
{ "lte": 40, "score": 8 },
|
96
|
-
{ "lte": 70, "score": 6 },
|
97
|
-
{ "lte": 90, "score": 3 },
|
98
|
-
{ "lte": 100, "score": 1 }
|
99
|
-
]
|
100
|
-
},
|
101
|
-
"coverage": {
|
102
|
-
"runBeforeScan": true,
|
103
|
-
"command": "npx jest --coverage --silent"
|
104
|
-
},
|
105
|
-
"weights": {
|
106
|
-
"BC": 0.25,
|
107
|
-
"CC": 0.15,
|
108
|
-
"ER": 0.15,
|
109
|
-
"ROI": 0.15,
|
110
|
-
"dependencyCount": 0.10,
|
111
|
-
"coverage": 0.20
|
112
|
-
},
|
113
|
-
"thresholds": { "P0": 8.0, "P1": 6.5, "P2": 4.5 },
|
114
|
-
"coverageBoost": { "enable": true, "threshold": 60, "scale": 0.5, "maxBoost": 0.5 },
|
115
|
-
"ccFusion": {
|
116
|
-
"useCognitive": true,
|
117
|
-
"cyclomaticT": 15,
|
118
|
-
"cognitiveT": 25,
|
119
|
-
"wC": 0.7,
|
120
|
-
"wK": 0.3,
|
121
|
-
"cap": 10
|
122
|
-
},
|
123
|
-
"ccAdjust": {
|
124
|
-
"depthW": 0.2,
|
125
|
-
"branchesW": 0.2,
|
126
|
-
"paramsW": 0.1,
|
127
|
-
"locBonusThreshold": 50,
|
128
|
-
"locBonus": 1
|
129
|
-
},
|
130
|
-
"bcKeywords": {
|
131
|
-
"10": ["price", "booking", "payment", "checkout"],
|
132
|
-
"9": ["recommend", "search"],
|
133
|
-
"8": ["filter", "list"],
|
134
|
-
"7": ["login", "order", "config"],
|
135
|
-
"5": ["navigation", "seo", "header"],
|
136
|
-
"3": ["log", "trace", "decor"]
|
137
|
-
},
|
138
|
-
"mainChainPaths": [],
|
139
|
-
"bcCapForNonMainChain": 8,
|
140
|
-
"ccMapping": {
|
141
|
-
"cyclomatic": [
|
142
|
-
{ "gt": 15, "score": 10 },
|
143
|
-
{ "gte": 11, "lte": 15, "score": 10 },
|
144
|
-
{ "gte": 6, "lte": 10, "score": 9 },
|
145
|
-
{ "gte": 3, "lte": 5, "score": 7 },
|
146
|
-
{ "lte": 2, "score": 6 }
|
147
|
-
],
|
148
|
-
"adjustments": [
|
149
|
-
{ "field": "maxDepth", "op": ">=", "value": 4, "delta": 1 },
|
150
|
-
{ "field": "branches", "op": ">=", "value": 12, "delta": 1 },
|
151
|
-
{ "field": "params", "op": ">=", "value": 6, "delta": 1 },
|
152
|
-
{ "field": "statements", "op": ">=", "value": 80, "delta": 1 },
|
153
|
-
{ "field": "cognitive", "op": ">=", "value": 25, "delta": 1 }
|
154
|
-
],
|
155
|
-
"cap": 10,
|
156
|
-
"platformAdjust": { "delta": 1, "cap": 10, "skipIfLikelihoodGte": 4 }
|
157
|
-
},
|
158
|
-
"fallbackMapping": {
|
159
|
-
"conditions": [
|
160
|
-
{ "gt": 12, "score": 10 },
|
161
|
-
{ "gte": 8, "lte": 12, "score": 8 },
|
162
|
-
{ "gte": 5, "lte": 7, "score": 6 },
|
163
|
-
{ "gte": 3, "lte": 4, "score": 4 },
|
164
|
-
{ "lte": 2, "score": 2 }
|
165
|
-
],
|
166
|
-
"nesting": [{ "gte": 4, "delta": 2 }, { "eq": 3, "delta": 1 }],
|
167
|
-
"earlyReturns": [{ "gte": 4, "delta": 1 }],
|
168
|
-
"paramsOrSources": [
|
169
|
-
{ "paramsGte": 6, "delta": 1 },
|
170
|
-
{ "sourcesGte": 3, "delta": 1 }
|
171
|
-
],
|
172
|
-
"cap": 10
|
173
|
-
},
|
174
|
-
"depGraph": {
|
175
|
-
"enable": true,
|
176
|
-
"neighborCategoryBoost": 2,
|
177
|
-
"degreeBoost": 8
|
178
|
-
},
|
179
|
-
"erMatrix": {
|
180
|
-
"5": { "5": 10, "4": 9, "3": 8, "2": 7, "1": 6 },
|
181
|
-
"4": { "5": 9, "4": 8, "3": 7, "2": 6, "1": 5 },
|
182
|
-
"3": { "5": 8, "4": 7, "3": 6, "2": 5, "1": 4 },
|
183
|
-
"2": { "5": 7, "4": 6, "3": 5, "2": 4, "1": 3 },
|
184
|
-
"1": { "5": 6, "4": 5, "3": 4, "2": 3, "1": 2 }
|
185
|
-
},
|
186
|
-
"likelihoodRules": [
|
187
|
-
{ "field": "commits30d", "op": ">=", "value": 6, "score": 5 },
|
188
|
-
{ "field": "commits30d", "op": "between", "min": 3, "max": 5, "score": 4 },
|
189
|
-
{ "field": "commits30d", "op": "between", "min": 1, "max": 2, "score": 3 },
|
190
|
-
{ "field": "fallback90d", "op": "gt", "value": 0, "score": 2 },
|
191
|
-
{ "field": "fallback180dZero", "op": "eq", "value": true, "score": 1 }
|
192
|
-
],
|
193
|
-
"boostRules": {
|
194
|
-
"authors30dGte": 3,
|
195
|
-
"crossModule": true,
|
196
|
-
"multiPlatform": true,
|
197
|
-
"cap": 5
|
198
|
-
},
|
199
|
-
"crossModuleCategories": ["components", "hooks", "utils", "services", "pages"],
|
200
|
-
"hintMaps": {
|
201
|
-
"impactLocal": "configs/impact.local.json",
|
202
|
-
"roiLocal": "configs/roi.local.json"
|
203
|
-
},
|
204
|
-
"overrides": "reports/overrides.json",
|
205
|
-
"impactKeywords": {
|
206
|
-
"5": ["payment", "booking", "price"],
|
207
|
-
"4": ["filter", "list", "display"],
|
208
|
-
"3": ["interaction"],
|
209
|
-
"2": ["minor", "ui"],
|
210
|
-
"1": ["decor", "cosmetic"]
|
211
|
-
},
|
212
|
-
"testabilityRules": {
|
213
|
-
"pure": 10,
|
214
|
-
"injectable": 9,
|
215
|
-
"multiContext": 7,
|
216
|
-
"nativeOrNetwork": 5,
|
217
|
-
"needsUI": 3
|
218
|
-
},
|
219
|
-
"dependencyCountMapping": [
|
220
|
-
{ "gte": 10, "score": 10 },
|
221
|
-
{ "gte": 5, "lt": 10, "score": 10 },
|
222
|
-
{ "gte": 3, "lt": 5, "score": 9 },
|
223
|
-
{ "gte": 1, "lt": 3, "score": 7 },
|
224
|
-
{ "eq": 0, "score": 5 }
|
225
|
-
],
|
226
|
-
"fallbacks": { "BC": 6, "CC": 6, "ERLikelihood": 3, "Testability": 6 }
|
227
|
-
}
|