soloforge 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude_code/tools.d.ts.map +1 -1
- package/dist/adapters/claude_code/tools.js +8 -0
- package/dist/adapters/claude_code/tools.js.map +1 -1
- package/dist/engine/audit_verifier.d.ts +52 -0
- package/dist/engine/audit_verifier.d.ts.map +1 -0
- package/dist/engine/audit_verifier.js +96 -0
- package/dist/engine/audit_verifier.js.map +1 -0
- package/dist/engine/code_reviewer.d.ts +6 -0
- package/dist/engine/code_reviewer.d.ts.map +1 -1
- package/dist/engine/code_reviewer.js +73 -3
- package/dist/engine/code_reviewer.js.map +1 -1
- package/dist/engine/evolver.d.ts +9 -0
- package/dist/engine/evolver.d.ts.map +1 -1
- package/dist/engine/evolver.js +47 -1
- package/dist/engine/evolver.js.map +1 -1
- package/dist/engine/exploration.d.ts +13 -0
- package/dist/engine/exploration.d.ts.map +1 -1
- package/dist/engine/exploration.js +59 -1
- package/dist/engine/exploration.js.map +1 -1
- package/dist/engine/intent_expander.d.ts.map +1 -1
- package/dist/engine/intent_expander.js +10 -5
- package/dist/engine/intent_expander.js.map +1 -1
- package/dist/engine/io_controller.d.ts +87 -0
- package/dist/engine/io_controller.d.ts.map +1 -0
- package/dist/engine/io_controller.js +190 -0
- package/dist/engine/io_controller.js.map +1 -0
- package/dist/engine/knowledge_config_loader.d.ts +28 -0
- package/dist/engine/knowledge_config_loader.d.ts.map +1 -0
- package/dist/engine/knowledge_config_loader.js +72 -0
- package/dist/engine/knowledge_config_loader.js.map +1 -0
- package/dist/engine/knowledge_manager.d.ts.map +1 -1
- package/dist/engine/knowledge_manager.js +11 -1
- package/dist/engine/knowledge_manager.js.map +1 -1
- package/dist/engine/llm_gateway.d.ts +96 -0
- package/dist/engine/llm_gateway.d.ts.map +1 -0
- package/dist/engine/llm_gateway.js +220 -0
- package/dist/engine/llm_gateway.js.map +1 -0
- package/dist/engine/test_quality.d.ts +15 -1
- package/dist/engine/test_quality.d.ts.map +1 -1
- package/dist/engine/test_quality.js +235 -23
- package/dist/engine/test_quality.js.map +1 -1
- package/dist/engine/workspace_resumer.d.ts +37 -0
- package/dist/engine/workspace_resumer.d.ts.map +1 -0
- package/dist/engine/workspace_resumer.js +82 -0
- package/dist/engine/workspace_resumer.js.map +1 -0
- package/dist/knowledge/writer.d.ts +2 -0
- package/dist/knowledge/writer.d.ts.map +1 -1
- package/dist/knowledge/writer.js +12 -4
- package/dist/knowledge/writer.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { loadKnowledgeConfig, extractNumberRule } from "./knowledge_config_loader.js";
|
|
2
|
+
// ── 操作配置表 ──
|
|
3
|
+
const OPERATION_PROFILES = {
|
|
4
|
+
intent_refinement: { tier: "medium", estimated_tokens: 2000, description: "意图理解深化" },
|
|
5
|
+
solution_brainstorm: { tier: "heavy", estimated_tokens: 4000, description: "方案脑暴推演" },
|
|
6
|
+
verification_reasoning: { tier: "heavy", estimated_tokens: 3000, description: "复杂验证推理" },
|
|
7
|
+
code_generation: { tier: "heavy", estimated_tokens: 5000, description: "代码生成" },
|
|
8
|
+
test_generation: { tier: "medium", estimated_tokens: 3000, description: "测试用例生成" },
|
|
9
|
+
review_deep_analysis: { tier: "heavy", estimated_tokens: 4000, description: "深度审查分析" },
|
|
10
|
+
debug_analysis: { tier: "medium", estimated_tokens: 2500, description: "排障推理" },
|
|
11
|
+
};
|
|
12
|
+
/** 从知识库 decision_gateway.md 读取阈值,文件不存在时使用硬编码兜底 */
|
|
13
|
+
function loadGatewayConfig(projectPath) {
|
|
14
|
+
const config = loadKnowledgeConfig("patterns/core/decision_gateway.md", projectPath);
|
|
15
|
+
const body = config?.body ?? "";
|
|
16
|
+
return {
|
|
17
|
+
budget_total: extractNumberRule(body, "Budget_Total") ?? 100_000,
|
|
18
|
+
task_budget: extractNumberRule(body, "Task_Budget") ?? 30_000,
|
|
19
|
+
circuit_breaker_ratio: extractNumberRule(body, "Circuit_Breaker_Ratio") ?? 0.9,
|
|
20
|
+
heartbeat_interval: extractNumberRule(body, "Heartbeat_Interval") ?? 5000,
|
|
21
|
+
confidence_threshold: extractNumberRule(body, "Confidence_Threshold") ?? 0.95,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_CONFIG = loadGatewayConfig();
|
|
25
|
+
// ── 网关核心 ──
|
|
26
|
+
export class LLMGateway {
|
|
27
|
+
budgetTotal;
|
|
28
|
+
budgetUsed = 0;
|
|
29
|
+
taskBudget;
|
|
30
|
+
taskTokensUsed = 0;
|
|
31
|
+
currentTaskId = null;
|
|
32
|
+
callCount = 0;
|
|
33
|
+
blockedCount = 0;
|
|
34
|
+
auditLog = [];
|
|
35
|
+
circuitOpen = false;
|
|
36
|
+
heartbeatTimer = null;
|
|
37
|
+
heartbeatCallback = null;
|
|
38
|
+
operationStart = 0;
|
|
39
|
+
currentOperation = null;
|
|
40
|
+
config = DEFAULT_CONFIG;
|
|
41
|
+
constructor(budget, taskBudget) {
|
|
42
|
+
this.budgetTotal = budget ?? this.config.budget_total;
|
|
43
|
+
this.taskBudget = taskBudget ?? this.config.task_budget;
|
|
44
|
+
}
|
|
45
|
+
/** 设置心跳回调(用于 stdio 输出进度) */
|
|
46
|
+
setHeartbeatCallback(cb) {
|
|
47
|
+
this.heartbeatCallback = cb;
|
|
48
|
+
}
|
|
49
|
+
/** 开始新任务的 Token 计量 */
|
|
50
|
+
beginTask(taskId) {
|
|
51
|
+
this.currentTaskId = taskId;
|
|
52
|
+
this.taskTokensUsed = 0;
|
|
53
|
+
}
|
|
54
|
+
/** 结束任务计量 */
|
|
55
|
+
endTask() {
|
|
56
|
+
this.currentTaskId = null;
|
|
57
|
+
this.taskTokensUsed = 0;
|
|
58
|
+
this.stopHeartbeat();
|
|
59
|
+
}
|
|
60
|
+
request(operation, taskId) {
|
|
61
|
+
const profile = OPERATION_PROFILES[operation];
|
|
62
|
+
const tid = taskId ?? this.currentTaskId ?? undefined;
|
|
63
|
+
const entry = {
|
|
64
|
+
operation,
|
|
65
|
+
estimated_tokens: profile.estimated_tokens,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
blocked: false,
|
|
68
|
+
task_id: tid,
|
|
69
|
+
};
|
|
70
|
+
// 单任务阈值检查
|
|
71
|
+
if (this.taskTokensUsed + profile.estimated_tokens > this.taskBudget) {
|
|
72
|
+
this.blockedCount++;
|
|
73
|
+
entry.blocked = true;
|
|
74
|
+
this.auditLog.push(entry);
|
|
75
|
+
return {
|
|
76
|
+
allowed: false,
|
|
77
|
+
estimated_tokens: profile.estimated_tokens,
|
|
78
|
+
remaining_budget: this.budgetTotal - this.budgetUsed,
|
|
79
|
+
remaining_task_budget: this.taskBudget - this.taskTokensUsed,
|
|
80
|
+
reason: `单任务 Token 阈值触发: 当前任务已使用 ${this.taskTokensUsed}/${this.taskBudget},${profile.description} 需要 ~${profile.estimated_tokens}。建议拆分任务或减少范围`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// 全局熔断检查
|
|
84
|
+
if (this.circuitOpen || (this.budgetUsed / this.budgetTotal) >= this.config.circuit_breaker_ratio) {
|
|
85
|
+
this.circuitOpen = true;
|
|
86
|
+
this.blockedCount++;
|
|
87
|
+
entry.blocked = true;
|
|
88
|
+
this.auditLog.push(entry);
|
|
89
|
+
return {
|
|
90
|
+
allowed: false,
|
|
91
|
+
estimated_tokens: profile.estimated_tokens,
|
|
92
|
+
remaining_budget: this.budgetTotal - this.budgetUsed,
|
|
93
|
+
remaining_task_budget: this.taskBudget - this.taskTokensUsed,
|
|
94
|
+
reason: `Token 预告警: 已使用 ${this.budgetUsed}/${this.budgetTotal}(${((this.budgetUsed / this.budgetTotal) * 100).toFixed(0)}%)。${profile.description} 需要 ~${profile.estimated_tokens} Token,超出安全阈值`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (this.budgetUsed + profile.estimated_tokens > this.budgetTotal) {
|
|
98
|
+
this.blockedCount++;
|
|
99
|
+
entry.blocked = true;
|
|
100
|
+
this.auditLog.push(entry);
|
|
101
|
+
return {
|
|
102
|
+
allowed: false,
|
|
103
|
+
estimated_tokens: profile.estimated_tokens,
|
|
104
|
+
remaining_budget: this.budgetTotal - this.budgetUsed,
|
|
105
|
+
remaining_task_budget: this.taskBudget - this.taskTokensUsed,
|
|
106
|
+
reason: `Token 预算不足: 剩余 ${this.budgetTotal - this.budgetUsed},需要 ~${profile.estimated_tokens}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
this.budgetUsed += profile.estimated_tokens;
|
|
110
|
+
this.taskTokensUsed += profile.estimated_tokens;
|
|
111
|
+
this.callCount++;
|
|
112
|
+
this.auditLog.push(entry);
|
|
113
|
+
// 重型操作启动心跳
|
|
114
|
+
if (profile.tier === "heavy") {
|
|
115
|
+
this.startHeartbeat(operation);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
allowed: true,
|
|
119
|
+
estimated_tokens: profile.estimated_tokens,
|
|
120
|
+
remaining_budget: this.budgetTotal - this.budgetUsed,
|
|
121
|
+
remaining_task_budget: this.taskBudget - this.taskTokensUsed,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** 置信度闸口: 低于阈值禁止执行代码,必须调用 sf_brainstorm */
|
|
125
|
+
confidenceGate(confidence) {
|
|
126
|
+
const threshold = this.config.confidence_threshold;
|
|
127
|
+
if (confidence < threshold) {
|
|
128
|
+
return {
|
|
129
|
+
allowed: false,
|
|
130
|
+
threshold,
|
|
131
|
+
reason: `置信度 ${confidence.toFixed(2)} 低于阈值 ${threshold},禁止直接输出执行代码,必须调用 sf_brainstorm 展示三轨方案`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { allowed: true, threshold };
|
|
135
|
+
}
|
|
136
|
+
/** 标记操作完成,停止心跳 */
|
|
137
|
+
completeOperation() {
|
|
138
|
+
this.stopHeartbeat();
|
|
139
|
+
this.currentOperation = null;
|
|
140
|
+
}
|
|
141
|
+
reportActualUsage(operation, actualTokens) {
|
|
142
|
+
const profile = OPERATION_PROFILES[operation];
|
|
143
|
+
const diff = actualTokens - profile.estimated_tokens;
|
|
144
|
+
if (diff > 0) {
|
|
145
|
+
this.budgetUsed += diff;
|
|
146
|
+
this.taskTokensUsed += diff;
|
|
147
|
+
if ((this.budgetUsed / this.budgetTotal) >= this.config.circuit_breaker_ratio) {
|
|
148
|
+
this.circuitOpen = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.budgetUsed = Math.max(0, this.budgetUsed + diff);
|
|
153
|
+
this.taskTokensUsed = Math.max(0, this.taskTokensUsed + diff);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
getStatus() {
|
|
157
|
+
return {
|
|
158
|
+
budget_total: this.budgetTotal,
|
|
159
|
+
budget_used: this.budgetUsed,
|
|
160
|
+
budget_remaining: this.budgetTotal - this.budgetUsed,
|
|
161
|
+
usage_ratio: this.budgetUsed / this.budgetTotal,
|
|
162
|
+
circuit_open: this.circuitOpen,
|
|
163
|
+
call_count: this.callCount,
|
|
164
|
+
blocked_count: this.blockedCount,
|
|
165
|
+
task_tokens_used: this.taskTokensUsed,
|
|
166
|
+
audit_log: [...this.auditLog],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
reset(budget) {
|
|
170
|
+
this.budgetTotal = budget ?? this.config.budget_total;
|
|
171
|
+
this.budgetUsed = 0;
|
|
172
|
+
this.callCount = 0;
|
|
173
|
+
this.blockedCount = 0;
|
|
174
|
+
this.auditLog = [];
|
|
175
|
+
this.circuitOpen = false;
|
|
176
|
+
this.taskTokensUsed = 0;
|
|
177
|
+
this.stopHeartbeat();
|
|
178
|
+
}
|
|
179
|
+
static isLocalOperation(op) {
|
|
180
|
+
const LOCAL_OPS = [
|
|
181
|
+
"classify", "scope_resolve", "knowledge_match", "pattern_extract",
|
|
182
|
+
"drift_detect", "mutation_test", "coverage_scan", "dependency_scan",
|
|
183
|
+
"migration_check", "contract_check", "debt_scan", "build_run",
|
|
184
|
+
];
|
|
185
|
+
return LOCAL_OPS.includes(op);
|
|
186
|
+
}
|
|
187
|
+
static isAIOperation(op) {
|
|
188
|
+
return op in OPERATION_PROFILES;
|
|
189
|
+
}
|
|
190
|
+
static getProfile(op) {
|
|
191
|
+
return OPERATION_PROFILES[op];
|
|
192
|
+
}
|
|
193
|
+
// ── 流式心跳 ──
|
|
194
|
+
startHeartbeat(operation) {
|
|
195
|
+
this.stopHeartbeat();
|
|
196
|
+
this.operationStart = Date.now();
|
|
197
|
+
this.currentOperation = operation;
|
|
198
|
+
if (!this.heartbeatCallback)
|
|
199
|
+
return;
|
|
200
|
+
this.heartbeatTimer = setInterval(() => {
|
|
201
|
+
if (!this.heartbeatCallback || !this.currentOperation)
|
|
202
|
+
return;
|
|
203
|
+
const elapsed = Date.now() - this.operationStart;
|
|
204
|
+
const profile = OPERATION_PROFILES[this.currentOperation];
|
|
205
|
+
this.heartbeatCallback({
|
|
206
|
+
elapsed_ms: elapsed,
|
|
207
|
+
tokens_used: this.taskTokensUsed,
|
|
208
|
+
operation: this.currentOperation,
|
|
209
|
+
message: `[SoloForge Heartbeat] ${profile.description} 执行中... 已耗时 ${(elapsed / 1000).toFixed(0)}s,当前任务已用 ${this.taskTokensUsed}/${this.taskBudget} Token`,
|
|
210
|
+
});
|
|
211
|
+
}, this.config.heartbeat_interval);
|
|
212
|
+
}
|
|
213
|
+
stopHeartbeat() {
|
|
214
|
+
if (this.heartbeatTimer) {
|
|
215
|
+
clearInterval(this.heartbeatTimer);
|
|
216
|
+
this.heartbeatTimer = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=llm_gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm_gateway.js","sourceRoot":"","sources":["../../src/engine/llm_gateway.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,5 +12,19 @@ import type { TestQualityReport } from "../types.js";
|
|
|
12
12
|
* @param sourceContent - 可选的被测源码内容(用于变异测试)
|
|
13
13
|
* @returns 测试质量报告,包含总分(0-100)、各项检查结果和改进建议列表
|
|
14
14
|
*/
|
|
15
|
-
export declare function analyzeTestQuality(testContent: string, filename: string, sourceContent?: string): TestQualityReport;
|
|
15
|
+
export declare function analyzeTestQuality(testContent: string, filename: string, sourceContent?: string, coverageMap?: CoverageMap, sourceFilePath?: string): TestQualityReport;
|
|
16
|
+
/** lcov 报告解析结果: 文件路径 → 已覆盖行号集合 */
|
|
17
|
+
export type CoverageMap = Map<string, Set<number>>;
|
|
18
|
+
/**
|
|
19
|
+
* 解析 lcov.info 报告,提取每个文件的已覆盖行号。
|
|
20
|
+
* lcov 格式:
|
|
21
|
+
* SF:path/to/file.ts
|
|
22
|
+
* DA:10,1
|
|
23
|
+
* DA:25,0
|
|
24
|
+
* end_of_record
|
|
25
|
+
* DA 行中第二个字段 > 0 表示该行被执行过(已覆盖)。
|
|
26
|
+
* @param lcovContent - lcov.info 文件内容
|
|
27
|
+
* @returns 覆盖率映射(文件路径 → 已覆盖行号集合)
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseLcov(lcovContent: string): CoverageMap;
|
|
16
30
|
//# sourceMappingURL=test_quality.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test_quality.d.ts","sourceRoot":"","sources":["../../src/engine/test_quality.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"test_quality.d.ts","sourceRoot":"","sources":["../../src/engine/test_quality.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGvE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW,EACzB,cAAc,CAAC,EAAE,MAAM,GACtB,iBAAiB,CAuEnB;AAidD,kCAAkC;AAClC,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAyB1D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadKnowledgeConfig, extractNumberRule, extractListRule } from "./knowledge_config_loader.js";
|
|
1
2
|
/**
|
|
2
3
|
* 测试质量分析器 — 对测试文件进行纯规则维度的质量评估,包含断言密度、边界覆盖、
|
|
3
4
|
* 命名规范性、断言重复和场景覆盖五个检查维度。零 AI 依赖。
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
* @param sourceContent - 可选的被测源码内容(用于变异测试)
|
|
12
13
|
* @returns 测试质量报告,包含总分(0-100)、各项检查结果和改进建议列表
|
|
13
14
|
*/
|
|
14
|
-
export function analyzeTestQuality(testContent, filename, sourceContent) {
|
|
15
|
+
export function analyzeTestQuality(testContent, filename, sourceContent, coverageMap, sourceFilePath) {
|
|
15
16
|
const checks = [];
|
|
16
17
|
const suggestions = [];
|
|
17
18
|
// ── 1. assertion_density ──
|
|
@@ -41,9 +42,9 @@ export function analyzeTestQuality(testContent, filename, sourceContent) {
|
|
|
41
42
|
// ── 5. scenario_coverage ──
|
|
42
43
|
const scenarioCheck = checkScenarioCoverage();
|
|
43
44
|
checks.push(scenarioCheck);
|
|
44
|
-
// ── 6. mutation_audit (机制 4: 变异测试防造假) ──
|
|
45
|
+
// ── 6. mutation_audit (机制 4: 变异测试防造假 + 覆盖率联动) ──
|
|
45
46
|
if (sourceContent) {
|
|
46
|
-
const mutationCheck =
|
|
47
|
+
const mutationCheck = checkMutationResistanceWithCoverage(testContent, sourceContent, coverageMap, sourceFilePath);
|
|
47
48
|
checks.push(mutationCheck);
|
|
48
49
|
if (!mutationCheck.passed) {
|
|
49
50
|
suggestions.push("测试可能存在'自己证明自己'问题:变异后断言仍能通过。建议增加反向用例和精确断言");
|
|
@@ -288,6 +289,19 @@ function checkScenarioCoverage() {
|
|
|
288
289
|
detail: "需结合知识模板验收项进行交叉检查",
|
|
289
290
|
};
|
|
290
291
|
}
|
|
292
|
+
// ── 机制 4: 变异测试防造假(知识库驱动) ──
|
|
293
|
+
/** 从知识库 mutation_audit.md 读取变异审计配置 */
|
|
294
|
+
function loadMutationConfig() {
|
|
295
|
+
const config = loadKnowledgeConfig("patterns/core/mutation_audit.md");
|
|
296
|
+
const body = config?.body ?? "";
|
|
297
|
+
return {
|
|
298
|
+
precision_threshold: extractNumberRule(body, "Precision_Threshold") ?? 0.5,
|
|
299
|
+
max_sample_lines: 8,
|
|
300
|
+
whitelist_keywords: extractListRule(body, "变异白名单") ?? ["if", "else", "switch", "===", "!=="],
|
|
301
|
+
blacklist_keywords: extractListRule(body, "变异黑名单") ?? ["console.log", "import", "export", "debugger"],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const MUTATION_CONFIG = loadMutationConfig();
|
|
291
305
|
/** 预定义变异操作集 */
|
|
292
306
|
const MUTATION_OPERATORS = [
|
|
293
307
|
{
|
|
@@ -341,32 +355,85 @@ const MUTATION_OPERATORS = [
|
|
|
341
355
|
},
|
|
342
356
|
},
|
|
343
357
|
];
|
|
358
|
+
// ── 变异白名单: AST 级别节点过滤 ──
|
|
359
|
+
/**
|
|
360
|
+
* 判断一行代码是否属于可变异的白名单节点。
|
|
361
|
+
* 白名单: 条件分支(if/else/switch)、比较/逻辑操作符、return 语句、算术表达式。
|
|
362
|
+
* 黑名单: console.log、注释、import/export、纯字符串/变量名赋值、类型声明。
|
|
363
|
+
*/
|
|
364
|
+
function isMutationCandidate(line) {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
// 黑名单: 跳过注释
|
|
367
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*"))
|
|
368
|
+
return false;
|
|
369
|
+
// 黑名单: 跳过 import/export 声明
|
|
370
|
+
if (/^(import |export |from )/.test(trimmed))
|
|
371
|
+
return false;
|
|
372
|
+
// 黑名单: 跳过 console.log / console.warn / console.error 等无副作用日志
|
|
373
|
+
if (/console\.(log|warn|error|info|debug)\s*\(/.test(trimmed))
|
|
374
|
+
return false;
|
|
375
|
+
// 黑名单: 跳过纯类型声明(TypeScript interface/type/enum)
|
|
376
|
+
if (/^(interface |type |enum |declare )/.test(trimmed))
|
|
377
|
+
return false;
|
|
378
|
+
// 黑名单: 跳过纯字符串字面量行(无逻辑)
|
|
379
|
+
if (/^['"\`]/.test(trimmed) && trimmed.endsWith(';'))
|
|
380
|
+
return false;
|
|
381
|
+
// 黑名单: 跳过 debugger / breakpoint
|
|
382
|
+
if (/^debugger/.test(trimmed))
|
|
383
|
+
return false;
|
|
384
|
+
// 白名单: 条件分支 (if / else if / switch / case / ternary)
|
|
385
|
+
if (/\b(if|else|switch|case)\b/.test(trimmed) && /[(){}?]/.test(trimmed))
|
|
386
|
+
return true;
|
|
387
|
+
// 白名单: 比较操作符
|
|
388
|
+
if (/(===|!==|==|!=|>=|<=|>|<)/.test(trimmed))
|
|
389
|
+
return true;
|
|
390
|
+
// 白名单: 逻辑操作符 (&& ||)
|
|
391
|
+
if (/(&&|\|\|)/.test(trimmed))
|
|
392
|
+
return true;
|
|
393
|
+
// 白名单: return 语句(非 return void)
|
|
394
|
+
if (/\breturn\b/.test(trimmed) && !/return\s*;\s*$/.test(trimmed))
|
|
395
|
+
return true;
|
|
396
|
+
// 白名单: 布尔字面量在表达式中
|
|
397
|
+
if (/\b(true|false)\b/.test(trimmed) && /[=(){}?]/.test(trimmed))
|
|
398
|
+
return true;
|
|
399
|
+
// 白名单: 算术表达式在赋值/返回中
|
|
400
|
+
if (/[+\-*/%]/.test(trimmed) && /[=()]/.test(trimmed))
|
|
401
|
+
return true;
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
344
404
|
/**
|
|
345
405
|
* 变异测试防造假 — 通过对源码模拟变异,检查测试的断言是否精确到能检测变异。
|
|
346
|
-
* 策略:
|
|
406
|
+
* 策略: 从源码中提取白名单关键逻辑行(AST 级别过滤),执行变异,
|
|
407
|
+
* 检查测试中是否有断言针对这些逻辑的精确值。
|
|
347
408
|
* 若测试只含模糊断言(如 toBeTruthy),判定为可能存在"自己证明自己"问题。
|
|
348
409
|
*/
|
|
410
|
+
/** 伪随机数生成器 (基于种子,确保可复现且不由 AI 控制) */
|
|
411
|
+
function seededRandom(seed) {
|
|
412
|
+
let s = seed;
|
|
413
|
+
return () => {
|
|
414
|
+
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
415
|
+
return s / 0x7fffffff;
|
|
416
|
+
};
|
|
417
|
+
}
|
|
349
418
|
function checkMutationResistance(testContent, sourceContent) {
|
|
350
|
-
//
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
return trimmed.length > 0
|
|
354
|
-
&& !trimmed.startsWith("//")
|
|
355
|
-
&& !trimmed.startsWith("import ")
|
|
356
|
-
&& !trimmed.startsWith("export ")
|
|
357
|
-
&& (/===|!==|>=|<=|>|<|return|true|false/.test(trimmed));
|
|
358
|
-
});
|
|
359
|
-
if (keyLines.length === 0) {
|
|
419
|
+
// 提取白名单关键逻辑行(AST 级别过滤,跳过 console.log 等无副作用代码)
|
|
420
|
+
const allKeyLines = sourceContent.split("\n").filter(isMutationCandidate);
|
|
421
|
+
if (allKeyLines.length === 0) {
|
|
360
422
|
return {
|
|
361
423
|
dimension: "scenario_coverage",
|
|
362
424
|
passed: true,
|
|
363
425
|
detail: "无可变异的关键逻辑行,跳过变异测试",
|
|
364
426
|
};
|
|
365
427
|
}
|
|
428
|
+
// 强制随机抽取: 使用源码哈希作为种子,确保可复现但不由 AI 选择位置
|
|
429
|
+
// 从所有关键行中随机抽取 min(行数, 8) 行进行变异,防止 AI 避开弱点行
|
|
430
|
+
const seed = sourceContent.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
|
|
431
|
+
const rng = seededRandom(seed);
|
|
432
|
+
const sampleSize = Math.min(allKeyLines.length, 8);
|
|
433
|
+
const keyLines = seededSample(allKeyLines, sampleSize, rng);
|
|
366
434
|
// 检查测试中的断言精确度
|
|
367
435
|
const preciseAssertions = testContent.match(/\bexpect\s*\([^)]*\)\s*\.\s*(toBe|toEqual|toStrictEqual|toBeLessThan|toBeGreaterThan|toThrow|toThrowError)/g) ?? [];
|
|
368
436
|
const looseAssertions = testContent.match(/\bexpect\s*\([^)]*\)\s*\.\s*(toBeTruthy|toBeFalsy|toBeDefined|toBeUndefined|toBeDefined)/g) ?? [];
|
|
369
|
-
// 变异可检测性评估: 精确断言/总断言 比率
|
|
370
437
|
const totalAssertions = preciseAssertions.length + looseAssertions.length;
|
|
371
438
|
if (totalAssertions === 0) {
|
|
372
439
|
return {
|
|
@@ -376,38 +443,183 @@ function checkMutationResistance(testContent, sourceContent) {
|
|
|
376
443
|
};
|
|
377
444
|
}
|
|
378
445
|
const precisionRatio = preciseAssertions.length / totalAssertions;
|
|
379
|
-
//
|
|
446
|
+
// 对随机抽取的行执行变异操作,检查是否有对应检测能力
|
|
380
447
|
let mutationsApplied = 0;
|
|
381
448
|
let mutationsUndetected = 0;
|
|
382
|
-
for (const keyLine of keyLines
|
|
383
|
-
|
|
449
|
+
for (const keyLine of keyLines) {
|
|
450
|
+
// 每行随机选一个能产生有效变异的操作符(防止 AI 预测哪个操作符会被选中)
|
|
451
|
+
const shuffledOps = seededSample([...MUTATION_OPERATORS], MUTATION_OPERATORS.length, rng);
|
|
452
|
+
for (const mut of shuffledOps) {
|
|
384
453
|
const mutated = mut.mutate(keyLine);
|
|
385
454
|
if (mutated !== keyLine) {
|
|
386
455
|
mutationsApplied++;
|
|
387
|
-
// 简化检测: 如果测试中存在针对被变异值的精确断言,认为可检测
|
|
388
456
|
const mutatedValues = mutated.match(/(!==|===|<=|>=|>|<|\b\d+\b|true|false)/g) ?? [];
|
|
389
457
|
const hasDetection = mutatedValues.some((v) => testContent.includes(v));
|
|
390
458
|
if (!hasDetection) {
|
|
391
459
|
mutationsUndetected++;
|
|
392
460
|
}
|
|
461
|
+
// 每行最多应用 1 个变异操作符(随机化后无需覆盖全部)
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const passThreshold = MUTATION_CONFIG.precision_threshold;
|
|
467
|
+
const precisionOk = precisionRatio >= passThreshold;
|
|
468
|
+
const detectionOk = mutationsApplied === 0 || (mutationsUndetected / mutationsApplied) < passThreshold;
|
|
469
|
+
if (precisionOk && detectionOk) {
|
|
470
|
+
return {
|
|
471
|
+
dimension: "scenario_coverage",
|
|
472
|
+
passed: true,
|
|
473
|
+
detail: `变异测试通过(随机抽样 ${keyLines.length} 行)— 精确断言率 ${(precisionRatio * 100).toFixed(0)}%,${mutationsApplied} 次变异中 ${mutationsApplied - mutationsUndetected} 次可检测`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
dimension: "scenario_coverage",
|
|
478
|
+
passed: false,
|
|
479
|
+
detail: `变异测试未通过(随机抽样 ${keyLines.length} 行)— 精确断言率 ${(precisionRatio * 100).toFixed(0)}%(阈值 50%),${mutationsApplied} 次变异中 ${mutationsUndetected} 次不可检测。测试可能存在"自己证明自己"问题`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/** 基于 RNG 的随机抽样(Fisher-Yates 部分洗牌)*/
|
|
483
|
+
function seededSample(arr, count, rng) {
|
|
484
|
+
const result = [...arr];
|
|
485
|
+
const n = Math.min(count, result.length);
|
|
486
|
+
for (let i = 0; i < n; i++) {
|
|
487
|
+
const j = i + Math.floor(rng() * (result.length - i));
|
|
488
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
489
|
+
}
|
|
490
|
+
return result.slice(0, n);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 解析 lcov.info 报告,提取每个文件的已覆盖行号。
|
|
494
|
+
* lcov 格式:
|
|
495
|
+
* SF:path/to/file.ts
|
|
496
|
+
* DA:10,1
|
|
497
|
+
* DA:25,0
|
|
498
|
+
* end_of_record
|
|
499
|
+
* DA 行中第二个字段 > 0 表示该行被执行过(已覆盖)。
|
|
500
|
+
* @param lcovContent - lcov.info 文件内容
|
|
501
|
+
* @returns 覆盖率映射(文件路径 → 已覆盖行号集合)
|
|
502
|
+
*/
|
|
503
|
+
export function parseLcov(lcovContent) {
|
|
504
|
+
const map = new Map();
|
|
505
|
+
let currentFile = "";
|
|
506
|
+
for (const line of lcovContent.split("\n")) {
|
|
507
|
+
const sfMatch = line.match(/^SF:(.+)$/);
|
|
508
|
+
if (sfMatch) {
|
|
509
|
+
currentFile = sfMatch[1];
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const daMatch = line.match(/^DA:(\d+),(\d+)/);
|
|
513
|
+
if (daMatch && currentFile) {
|
|
514
|
+
const lineNum = parseInt(daMatch[1], 10);
|
|
515
|
+
const hitCount = parseInt(daMatch[2], 10);
|
|
516
|
+
if (hitCount > 0) {
|
|
517
|
+
if (!map.has(currentFile))
|
|
518
|
+
map.set(currentFile, new Set());
|
|
519
|
+
map.get(currentFile).add(lineNum);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (line === "end_of_record") {
|
|
523
|
+
currentFile = "";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return map;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* 带覆盖率联动的变异测试 — 只变异被测试覆盖的代码行。
|
|
530
|
+
* 若变异了未覆盖行,该变异无法被测试检测到,审计结果无效。
|
|
531
|
+
* @param testContent - 测试文件内容
|
|
532
|
+
* @param sourceContent - 被测源码内容
|
|
533
|
+
* @param coverageMap - 可选的覆盖率映射(文件 → 已覆盖行号),无则退化为全行变异
|
|
534
|
+
* @param sourceFilePath - 源码文件路径,用于在 coverageMap 中查找覆盖行
|
|
535
|
+
*/
|
|
536
|
+
function checkMutationResistanceWithCoverage(testContent, sourceContent, coverageMap, sourceFilePath) {
|
|
537
|
+
const sourceLines = sourceContent.split("\n");
|
|
538
|
+
const coveredLineNumbers = (sourceFilePath && coverageMap) ? coverageMap.get(sourceFilePath) : undefined;
|
|
539
|
+
// 提取白名单关键逻辑行,并记录行号
|
|
540
|
+
const candidates = [];
|
|
541
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
542
|
+
if (isMutationCandidate(sourceLines[i])) {
|
|
543
|
+
candidates.push({ line: sourceLines[i], lineNum: i + 1 });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (candidates.length === 0) {
|
|
547
|
+
return {
|
|
548
|
+
dimension: "scenario_coverage",
|
|
549
|
+
passed: true,
|
|
550
|
+
detail: "无可变异的关键逻辑行,跳过变异测试",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// 覆盖率过滤: 只保留被测试覆盖的行
|
|
554
|
+
let filteredCandidates = candidates;
|
|
555
|
+
const hasCoverageData = coverageMap !== undefined && sourceFilePath !== undefined;
|
|
556
|
+
if (hasCoverageData) {
|
|
557
|
+
const covered = coveredLineNumbers ?? new Set();
|
|
558
|
+
const beforeCount = candidates.length;
|
|
559
|
+
if (covered.size === 0) {
|
|
560
|
+
return {
|
|
561
|
+
dimension: "scenario_coverage",
|
|
562
|
+
passed: false,
|
|
563
|
+
detail: `变异审计无效: ${beforeCount} 个可变异行均未被测试覆盖(覆盖率联动)`,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
filteredCandidates = candidates.filter((c) => covered.has(c.lineNum));
|
|
567
|
+
if (filteredCandidates.length === 0) {
|
|
568
|
+
return {
|
|
569
|
+
dimension: "scenario_coverage",
|
|
570
|
+
passed: false,
|
|
571
|
+
detail: `变异审计无效: ${beforeCount} 个可变异行均未被测试覆盖(覆盖率联动)`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// 随机抽样
|
|
576
|
+
const seed = sourceContent.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
|
|
577
|
+
const rng = seededRandom(seed);
|
|
578
|
+
const sampleSize = Math.min(filteredCandidates.length, 8);
|
|
579
|
+
const keyLines = seededSample(filteredCandidates, sampleSize, rng);
|
|
580
|
+
// 断言精确度检查 — 使用宽松正则以支持嵌套括号如 expect(fn(a, b))
|
|
581
|
+
const preciseAssertions = testContent.match(/\bexpect\b.*?\.(toBe|toEqual|toStrictEqual|toBeLessThan|toBeGreaterThan|toThrow|toThrowError)\s*\(/g) ?? [];
|
|
582
|
+
const looseAssertions = testContent.match(/\bexpect\b.*?\.(toBeTruthy|toBeFalsy|toBeDefined|toBeUndefined)\s*\(/g) ?? [];
|
|
583
|
+
const totalAssertions = preciseAssertions.length + looseAssertions.length;
|
|
584
|
+
if (totalAssertions === 0) {
|
|
585
|
+
return {
|
|
586
|
+
dimension: "scenario_coverage",
|
|
587
|
+
passed: false,
|
|
588
|
+
detail: "无断言,无法检测源码变异(变异测试防造假失败)",
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const precisionRatio = preciseAssertions.length / totalAssertions;
|
|
592
|
+
let mutationsApplied = 0;
|
|
593
|
+
let mutationsUndetected = 0;
|
|
594
|
+
for (const item of keyLines) {
|
|
595
|
+
const shuffledOps = seededSample([...MUTATION_OPERATORS], MUTATION_OPERATORS.length, rng);
|
|
596
|
+
for (const mut of shuffledOps) {
|
|
597
|
+
const mutated = mut.mutate(item.line);
|
|
598
|
+
if (mutated !== item.line) {
|
|
599
|
+
mutationsApplied++;
|
|
600
|
+
const mutatedValues = mutated.match(/(!==|===|<=|>=|>|<|\b\d+\b|true|false)/g) ?? [];
|
|
601
|
+
const hasDetection = mutatedValues.some((v) => testContent.includes(v));
|
|
602
|
+
if (!hasDetection)
|
|
603
|
+
mutationsUndetected++;
|
|
604
|
+
break;
|
|
393
605
|
}
|
|
394
606
|
}
|
|
395
607
|
}
|
|
396
|
-
|
|
397
|
-
const passThreshold = 0.5;
|
|
608
|
+
const passThreshold = MUTATION_CONFIG.precision_threshold;
|
|
398
609
|
const precisionOk = precisionRatio >= passThreshold;
|
|
399
610
|
const detectionOk = mutationsApplied === 0 || (mutationsUndetected / mutationsApplied) < passThreshold;
|
|
611
|
+
const coverageNote = hasCoverageData ? `(覆盖率联动: ${filteredCandidates.length}/${candidates.length} 可变异行已被覆盖)` : "";
|
|
400
612
|
if (precisionOk && detectionOk) {
|
|
401
613
|
return {
|
|
402
614
|
dimension: "scenario_coverage",
|
|
403
615
|
passed: true,
|
|
404
|
-
detail:
|
|
616
|
+
detail: `变异测试通过(随机抽样 ${keyLines.length} 行${coverageNote})— 精确断言率 ${(precisionRatio * 100).toFixed(0)}%,${mutationsApplied} 次变异中 ${mutationsApplied - mutationsUndetected} 次可检测`,
|
|
405
617
|
};
|
|
406
618
|
}
|
|
407
619
|
return {
|
|
408
620
|
dimension: "scenario_coverage",
|
|
409
621
|
passed: false,
|
|
410
|
-
detail:
|
|
622
|
+
detail: `变异测试未通过(随机抽样 ${keyLines.length} 行${coverageNote})— 精确断言率 ${(precisionRatio * 100).toFixed(0)}%(阈值 50%),${mutationsApplied} 次变异中 ${mutationsUndetected} 次不可检测。测试可能存在"自己证明自己"问题`,
|
|
411
623
|
};
|
|
412
624
|
}
|
|
413
625
|
//# sourceMappingURL=test_quality.js.map
|