@dommaker/harness 1.5.0 → 1.7.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/CHANGELOG.md +26 -0
- package/README.md +0 -2
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +1 -32
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/passes-gate.d.ts +0 -2
- package/dist/cli/commands/passes-gate.d.ts.map +1 -1
- package/dist/cli/commands/passes-gate.js +0 -1
- package/dist/cli/commands/passes-gate.js.map +1 -1
- package/dist/cli/commands/performance.d.ts +0 -4
- package/dist/cli/commands/performance.d.ts.map +1 -1
- package/dist/cli/commands/performance.js +10 -20
- package/dist/cli/commands/performance.js.map +1 -1
- package/dist/cli/commands/report.d.ts +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +2 -55
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +4 -8
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/context/index.d.ts +2 -5
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -8
- package/dist/context/index.js.map +1 -1
- package/dist/context/token-budget.d.ts +0 -29
- package/dist/context/token-budget.d.ts.map +1 -1
- package/dist/context/token-budget.js +1 -65
- package/dist/context/token-budget.js.map +1 -1
- package/dist/context/types.d.ts +0 -1
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +0 -9
- package/dist/context/types.js.map +1 -1
- package/dist/core/constraints/checker.d.ts +14 -0
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +22 -2
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/git-evidence.d.ts +6 -3
- package/dist/core/constraints/git-evidence.d.ts.map +1 -1
- package/dist/core/constraints/git-evidence.js +6 -3
- package/dist/core/constraints/git-evidence.js.map +1 -1
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -9
- package/dist/core/index.js.map +1 -1
- package/dist/core/spec/validator.d.ts +2 -0
- package/dist/core/spec/validator.d.ts.map +1 -1
- package/dist/core/spec/validator.js +9 -11
- package/dist/core/spec/validator.js.map +1 -1
- package/dist/core/validators/index.d.ts +1 -1
- package/dist/core/validators/index.d.ts.map +1 -1
- package/dist/core/validators/passes-gate.d.ts +1 -15
- package/dist/core/validators/passes-gate.d.ts.map +1 -1
- package/dist/core/validators/passes-gate.js +2 -127
- package/dist/core/validators/passes-gate.js.map +1 -1
- package/dist/failure/index.d.ts +0 -1
- package/dist/failure/index.d.ts.map +1 -1
- package/dist/failure/index.js +1 -7
- package/dist/failure/index.js.map +1 -1
- package/dist/gates/definitions.d.ts.map +1 -1
- package/dist/gates/definitions.js +0 -4
- package/dist/gates/definitions.js.map +1 -1
- package/dist/gates/performance.d.ts +7 -22
- package/dist/gates/performance.d.ts.map +1 -1
- package/dist/gates/performance.js +7 -89
- package/dist/gates/performance.js.map +1 -1
- package/dist/gates/types.d.ts +2 -8
- package/dist/gates/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -22
- package/dist/index.js.map +1 -1
- package/dist/knowledge/index.d.ts +0 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +1 -3
- package/dist/knowledge/index.js.map +1 -1
- package/dist/monitoring/trace-analyzer.d.ts +25 -10
- package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
- package/dist/monitoring/trace-analyzer.js +147 -116
- package/dist/monitoring/trace-analyzer.js.map +1 -1
- package/dist/types/passes-gate.d.ts +0 -32
- package/dist/types/passes-gate.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/passes-gate-check.test.ts +20 -4
- package/src/__tests__/passes-gate.test.ts +5 -99
- package/src/__tests__/performance-gate.test.ts +3 -11
- package/src/__tests__/public-exports.test.ts +68 -16
- package/src/__tests__/public-type-surface.test.ts +359 -0
- package/src/__tests__/trace-analyzer.test.ts +52 -2
- package/src/cli/commands/CONTEXT.md +3 -2
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +158 -0
- package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
- package/src/cli/commands/__tests__/performance.test.ts +5 -16
- package/src/cli/commands/__tests__/registry.test.ts +46 -3
- package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
- package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
- package/src/cli/commands/__tests__/report.test.ts +0 -20
- package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
- package/src/cli/commands/__tests__/status.test.ts +66 -148
- package/src/cli/commands/definitions.ts +1 -32
- package/src/cli/commands/passes-gate.ts +0 -3
- package/src/cli/commands/performance.ts +13 -27
- package/src/cli/commands/report.ts +3 -61
- package/src/cli/commands/status.ts +5 -10
- package/src/context/CONTEXT.md +2 -3
- package/src/context/__tests__/token-budget.test.ts +1 -81
- package/src/context/index.ts +2 -7
- package/src/context/token-budget.ts +0 -73
- package/src/context/types.ts +0 -9
- package/src/core/CONTEXT.md +5 -6
- package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
- package/src/core/constraints/checker.ts +33 -2
- package/src/core/constraints/git-evidence.ts +6 -3
- package/src/core/index.ts +0 -12
- package/src/core/spec/__tests__/validator.test.ts +24 -0
- package/src/core/spec/validator.ts +9 -10
- package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
- package/src/core/validators/index.ts +0 -2
- package/src/core/validators/passes-gate.ts +3 -152
- package/src/failure/CONTEXT.md +8 -6
- package/src/failure/index.ts +0 -10
- package/src/gates/CONTEXT.md +3 -2
- package/src/gates/__tests__/performance.test.ts +6 -74
- package/src/gates/definitions.ts +0 -4
- package/src/gates/performance.ts +9 -111
- package/src/gates/types.ts +2 -8
- package/src/index.ts +1 -37
- package/src/knowledge/CONTEXT.md +1 -2
- package/src/knowledge/index.ts +0 -1
- package/src/monitoring/CONTEXT.md +2 -2
- package/src/monitoring/trace-analyzer.ts +160 -126
- package/src/types/passes-gate.ts +0 -43
- package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
- package/dist/cli/commands/analyze-sessions.d.ts +0 -20
- package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
- package/dist/cli/commands/analyze-sessions.js +0 -290
- package/dist/cli/commands/analyze-sessions.js.map +0 -1
- package/dist/cli/commands/update-user-model.d.ts +0 -30
- package/dist/cli/commands/update-user-model.d.ts.map +0 -1
- package/dist/cli/commands/update-user-model.js +0 -406
- package/dist/cli/commands/update-user-model.js.map +0 -1
- package/dist/cli/session-mining/corrections.d.ts +0 -17
- package/dist/cli/session-mining/corrections.d.ts.map +0 -1
- package/dist/cli/session-mining/corrections.js +0 -58
- package/dist/cli/session-mining/corrections.js.map +0 -1
- package/dist/cli/session-mining/index.d.ts +0 -13
- package/dist/cli/session-mining/index.d.ts.map +0 -1
- package/dist/cli/session-mining/index.js +0 -29
- package/dist/cli/session-mining/index.js.map +0 -1
- package/dist/cli/session-mining/text.d.ts +0 -31
- package/dist/cli/session-mining/text.d.ts.map +0 -1
- package/dist/cli/session-mining/text.js +0 -126
- package/dist/cli/session-mining/text.js.map +0 -1
- package/dist/cli/session-mining/transcript.d.ts +0 -34
- package/dist/cli/session-mining/transcript.d.ts.map +0 -1
- package/dist/cli/session-mining/transcript.js +0 -130
- package/dist/cli/session-mining/transcript.js.map +0 -1
- package/dist/context/compaction.d.ts +0 -57
- package/dist/context/compaction.d.ts.map +0 -1
- package/dist/context/compaction.js +0 -165
- package/dist/context/compaction.js.map +0 -1
- package/dist/core/session/clean-state.d.ts +0 -17
- package/dist/core/session/clean-state.d.ts.map +0 -1
- package/dist/core/session/clean-state.js +0 -192
- package/dist/core/session/clean-state.js.map +0 -1
- package/dist/core/session/index.d.ts +0 -6
- package/dist/core/session/index.d.ts.map +0 -1
- package/dist/core/session/index.js +0 -15
- package/dist/core/session/index.js.map +0 -1
- package/dist/core/session/startup.d.ts +0 -47
- package/dist/core/session/startup.d.ts.map +0 -1
- package/dist/core/session/startup.js +0 -263
- package/dist/core/session/startup.js.map +0 -1
- package/dist/failure/constraint-handler.d.ts +0 -83
- package/dist/failure/constraint-handler.d.ts.map +0 -1
- package/dist/failure/constraint-handler.js +0 -138
- package/dist/failure/constraint-handler.js.map +0 -1
- package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
- package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
- package/dist/knowledge/lifecycle-hooks.js +0 -143
- package/dist/knowledge/lifecycle-hooks.js.map +0 -1
- package/dist/types/session.d.ts +0 -113
- package/dist/types/session.d.ts.map +0 -1
- package/dist/types/session.js +0 -6
- package/dist/types/session.js.map +0 -1
- package/src/__tests__/clean-state-extra.test.ts +0 -301
- package/src/__tests__/clean-state.test.ts +0 -169
- package/src/__tests__/constraint-handler.test.ts +0 -132
- package/src/__tests__/passes-gate-extra.test.ts +0 -93
- package/src/__tests__/session-startup.test.ts +0 -334
- package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
- package/src/cli/commands/__tests__/update-user-model.test.ts +0 -268
- package/src/cli/commands/analyze-sessions.ts +0 -335
- package/src/cli/commands/update-user-model.ts +0 -476
- package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
- package/src/cli/session-mining/corrections.ts +0 -56
- package/src/cli/session-mining/index.ts +0 -13
- package/src/cli/session-mining/text.ts +0 -117
- package/src/cli/session-mining/transcript.ts +0 -125
- package/src/context/__tests__/compaction.test.ts +0 -169
- package/src/context/compaction.ts +0 -201
- package/src/core/session/__tests__/startup.test.ts +0 -281
- package/src/core/session/clean-state.ts +0 -178
- package/src/core/session/index.ts +0 -15
- package/src/core/session/startup.ts +0 -253
- package/src/failure/constraint-handler.ts +0 -171
- package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
- package/src/knowledge/lifecycle-hooks.ts +0 -202
- package/src/types/session.ts +0 -132
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
* 「过了没」的判定依据不在本文件:唯一入口是 test-output.ts 的 judgeTestRun(ADR-0014)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { execAsync
|
|
10
|
+
import { execAsync } from '../../utils/exec';
|
|
11
11
|
import { judgeTestRun } from './test-output';
|
|
12
12
|
import * as fs from 'fs/promises';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import type {
|
|
15
15
|
PassesGateConfig,
|
|
16
|
-
PassesGateResult,
|
|
17
16
|
TaskTestResult,
|
|
18
|
-
DynamicTask,
|
|
19
17
|
TestResult,
|
|
20
18
|
PassesGateCheckResult,
|
|
21
19
|
PassesGateViolation,
|
|
@@ -30,24 +28,8 @@ const DEFAULT_CONFIG: Required<PassesGateConfig> = {
|
|
|
30
28
|
testCommand: '',
|
|
31
29
|
requireEvidence: true,
|
|
32
30
|
allowPartialPass: false,
|
|
33
|
-
maxRetries: 2,
|
|
34
|
-
retryDelay: 1000,
|
|
35
31
|
};
|
|
36
32
|
|
|
37
|
-
/**
|
|
38
|
-
* 测试文件保护模式
|
|
39
|
-
*/
|
|
40
|
-
const PROTECTED_TEST_PATTERNS = [
|
|
41
|
-
'**/*.test.ts',
|
|
42
|
-
'**/*.test.tsx',
|
|
43
|
-
'**/*.test.js',
|
|
44
|
-
'**/*.spec.ts',
|
|
45
|
-
'**/*.spec.tsx',
|
|
46
|
-
'**/*.spec.js',
|
|
47
|
-
'**/tests/**',
|
|
48
|
-
'**/__tests__/**',
|
|
49
|
-
];
|
|
50
|
-
|
|
51
33
|
/**
|
|
52
34
|
* 检测项目的测试命令(唯一正本,CLI 与库调用共消费,架构评审 A2)
|
|
53
35
|
*
|
|
@@ -98,7 +80,6 @@ export async function detectTestCommand(projectPath: string): Promise<string | u
|
|
|
98
80
|
*/
|
|
99
81
|
export class PassesGate {
|
|
100
82
|
private config: Required<PassesGateConfig>;
|
|
101
|
-
private testResults: Map<string, TaskTestResult> = new Map();
|
|
102
83
|
|
|
103
84
|
constructor(config: Partial<PassesGateConfig> = {}) {
|
|
104
85
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -170,99 +151,6 @@ export class PassesGate {
|
|
|
170
151
|
};
|
|
171
152
|
}
|
|
172
153
|
|
|
173
|
-
/**
|
|
174
|
-
* 设置任务通过状态
|
|
175
|
-
* 核心方法:强制测试验证
|
|
176
|
-
*/
|
|
177
|
-
async setPasses(
|
|
178
|
-
taskId: string,
|
|
179
|
-
value: boolean,
|
|
180
|
-
workDir: string,
|
|
181
|
-
task?: DynamicTask
|
|
182
|
-
): Promise<PassesGateResult> {
|
|
183
|
-
if (!this.config.enabled) {
|
|
184
|
-
return {
|
|
185
|
-
taskId,
|
|
186
|
-
allowed: true,
|
|
187
|
-
attempts: 0,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 如果设置为 false,直接允许
|
|
192
|
-
if (value === false) {
|
|
193
|
-
return {
|
|
194
|
-
taskId,
|
|
195
|
-
allowed: true,
|
|
196
|
-
testResult: {
|
|
197
|
-
passed: false,
|
|
198
|
-
command: 'manual',
|
|
199
|
-
timestamp: new Date(),
|
|
200
|
-
},
|
|
201
|
-
attempts: 0,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 如果设置为 true,必须运行测试
|
|
206
|
-
let attempts = 0;
|
|
207
|
-
let lastError: string | undefined;
|
|
208
|
-
let testResult: TaskTestResult | undefined;
|
|
209
|
-
|
|
210
|
-
while (attempts <= this.config.maxRetries) {
|
|
211
|
-
attempts++;
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
testResult = await this.runTest(workDir, task);
|
|
215
|
-
|
|
216
|
-
if (testResult.passed) {
|
|
217
|
-
// 记录测试结果
|
|
218
|
-
this.testResults.set(taskId, testResult);
|
|
219
|
-
|
|
220
|
-
// 如果要求证据,验证证据存在
|
|
221
|
-
if (this.config.requireEvidence && testResult.evidence) {
|
|
222
|
-
const evidenceExists = await this.verifyEvidence(testResult.evidence, workDir);
|
|
223
|
-
if (!evidenceExists) {
|
|
224
|
-
return {
|
|
225
|
-
taskId,
|
|
226
|
-
allowed: false,
|
|
227
|
-
error: 'Test evidence not found',
|
|
228
|
-
testResult,
|
|
229
|
-
attempts,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
taskId,
|
|
236
|
-
allowed: true,
|
|
237
|
-
testResult,
|
|
238
|
-
attempts,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
lastError = `Tests failed: ${testResult.failures?.join(', ') || 'Unknown error'}`;
|
|
243
|
-
|
|
244
|
-
// 等待后重试
|
|
245
|
-
if (attempts <= this.config.maxRetries) {
|
|
246
|
-
await delay(this.config.retryDelay);
|
|
247
|
-
}
|
|
248
|
-
} catch (error: any) {
|
|
249
|
-
lastError = error.message;
|
|
250
|
-
|
|
251
|
-
if (attempts <= this.config.maxRetries) {
|
|
252
|
-
await delay(this.config.retryDelay);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
taskId,
|
|
259
|
-
allowed: false,
|
|
260
|
-
error: lastError,
|
|
261
|
-
testResult,
|
|
262
|
-
attempts,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
154
|
/**
|
|
267
155
|
* 运行测试(CLI 使用)
|
|
268
156
|
*
|
|
@@ -294,7 +182,7 @@ export class PassesGate {
|
|
|
294
182
|
};
|
|
295
183
|
}
|
|
296
184
|
|
|
297
|
-
const result = await this.runTest(workDir,
|
|
185
|
+
const result = await this.runTest(workDir, testCommand);
|
|
298
186
|
const duration = Date.now() - startTime;
|
|
299
187
|
|
|
300
188
|
// 解析测试数量
|
|
@@ -329,7 +217,7 @@ export class PassesGate {
|
|
|
329
217
|
/**
|
|
330
218
|
* 运行测试
|
|
331
219
|
*/
|
|
332
|
-
private async runTest(workDir: string,
|
|
220
|
+
private async runTest(workDir: string, command?: string): Promise<TaskTestResult> {
|
|
333
221
|
const testCommand = command || this.config.testCommand || await detectTestCommand(workDir);
|
|
334
222
|
const timestamp = new Date();
|
|
335
223
|
|
|
@@ -404,43 +292,6 @@ export class PassesGate {
|
|
|
404
292
|
}
|
|
405
293
|
}
|
|
406
294
|
|
|
407
|
-
/**
|
|
408
|
-
* 检测是否修改了测试文件
|
|
409
|
-
*/
|
|
410
|
-
async checkTestFileChanges(workDir: string): Promise<string[]> {
|
|
411
|
-
const changedTestFiles: string[] = [];
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
const { stdout } = await execAsync('git diff --name-only', { cwd: workDir });
|
|
415
|
-
const changedFiles = stdout.split('\n').filter(Boolean);
|
|
416
|
-
|
|
417
|
-
// 简单的模式匹配
|
|
418
|
-
for (const file of changedFiles) {
|
|
419
|
-
const isTestFile = PROTECTED_TEST_PATTERNS.some(() => {
|
|
420
|
-
return file.includes('.test.') ||
|
|
421
|
-
file.includes('.spec.') ||
|
|
422
|
-
file.includes('/tests/') ||
|
|
423
|
-
file.includes('/__tests__/');
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
if (isTestFile) {
|
|
427
|
-
changedTestFiles.push(file);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
} catch {
|
|
431
|
-
// Git 不可用,跳过检查
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return changedTestFiles;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 获取任务的测试结果
|
|
439
|
-
*/
|
|
440
|
-
getTestResult(taskId: string): TaskTestResult | undefined {
|
|
441
|
-
return this.testResults.get(taskId);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
295
|
}
|
|
445
296
|
|
|
446
297
|
/**
|
package/src/failure/CONTEXT.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# failure/
|
|
2
2
|
|
|
3
3
|
## 职责
|
|
4
|
-
失败处理:错误分类(可扩展规则)、失败记录(文件存储)
|
|
4
|
+
失败处理:错误分类(可扩展规则)、失败记录(文件存储)。
|
|
5
5
|
|
|
6
6
|
## 核心导出
|
|
7
7
|
- `ErrorClassifier` — 错误分类器(可配置规则)
|
|
8
8
|
- `FailureRecorder` — 失败记录器(文件存储)
|
|
9
|
-
- `ConstraintViolationHandler` — 约束违规处理: block / collect / safeBoolean 三种策略
|
|
10
9
|
- `types.ts` — 错误分类/记录类型定义
|
|
11
10
|
|
|
12
11
|
## 依赖关系
|
|
13
|
-
- 依赖 `src/types
|
|
14
|
-
-
|
|
15
|
-
- 被 `src/cli/commands/report.ts` 消费(executeWithCollect),并经包根导出
|
|
12
|
+
- 依赖 `src/types/` 错误分类/记录类型
|
|
13
|
+
- 经包根导出(ADR-0003 显式清单)
|
|
16
14
|
|
|
17
15
|
## 约定
|
|
18
16
|
- 分类规则可扩展(自定义 ErrorClassifierConfig)
|
|
@@ -20,5 +18,9 @@
|
|
|
20
18
|
- 不包含业务逻辑,只提供通用能力
|
|
21
19
|
|
|
22
20
|
## 注意事项
|
|
23
|
-
- S4 统一处理策略:executeWithBlock / executeWithCollect / executeWithSafeBoolean
|
|
24
21
|
- 分类器基于规则匹配,不调用 LLM
|
|
22
|
+
- S4 约束违规处理策略模块(ConstraintViolationHandler / executeWithBlock /
|
|
23
|
+
executeWithCollect / executeWithSafeBoolean)已删除(架构评审候选1):
|
|
24
|
+
COLLECT 对真实 checker 结构上不可交付(checker 首个铁律违规即 throw,
|
|
25
|
+
handler 的 catch 只能吞证据造假绿)。语义归宿——阻断 = `checkConstraints` 本体,
|
|
26
|
+
收集 = `checker.collectConstraints`(report 直调);BLOCK/SAFE_BOOLEAN 零生产消费者随迁
|
package/src/failure/index.ts
CHANGED
|
@@ -23,13 +23,3 @@ export {
|
|
|
23
23
|
createFailureRecorder,
|
|
24
24
|
type FailureRecorderConfig,
|
|
25
25
|
} from './recorder';
|
|
26
|
-
|
|
27
|
-
// 约束违规处理(S4:统一处理策略)
|
|
28
|
-
export {
|
|
29
|
-
ConstraintViolationHandler,
|
|
30
|
-
executeWithBlock,
|
|
31
|
-
executeWithCollect,
|
|
32
|
-
executeWithSafeBoolean,
|
|
33
|
-
type ViolationStrategy,
|
|
34
|
-
type ViolationHandlingResult,
|
|
35
|
-
} from './constraint-handler';
|
package/src/gates/CONTEXT.md
CHANGED
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
- deny 单调是接口契约:决策浅冻结,下游不得改写上游决策
|
|
24
24
|
- `runGates` 是 Gate 链语义的**参考实现**(deny 单调 / ask fail-closed / 全决策浅冻结):仓内暂无生产调用方(harness CLI 文件驱动逐命令执行,studio `runCompletionGuards` 经 checker-gate 复用语义但不 import),消费方只有测试。「deny 后短路跳过剩余门禁」类优化在出现真实消费者前不做——它与「按执行顺序的全部决策」报告契约直接冲突,短路等于改 GateResult 语义(harness#115 裁决,防后续评审重复提议)
|
|
25
25
|
- ask 枚举预留:暂无实现,runGates fail-closed 按 deny 计
|
|
26
|
-
- **收到的根要传到自己每个 IO/执行点(harness#95)**:门禁只认 `context.projectPath`,内部相对子路径一律锚到它(`SpecAcceptanceGate` 的 tasksPath、`ContractGate` 的 contractPath、e2e/scan
|
|
26
|
+
- **收到的根要传到自己每个 IO/执行点(harness#95)**:门禁只认 `context.projectPath`,内部相对子路径一律锚到它(`SpecAcceptanceGate` 的 tasksPath、`ContractGate` 的 contractPath、e2e/scan 的 `cwd`),禁止「根已传入却又取 cwd」、禁止 `xxxPath: './…'` 相对默认值——否则 `-p` 半失效且表现为假绿(acceptance 的「无 tasks.yml 即跳过」= passed:true)。本层唯一保留的 cwd 站点是 `checker-gate` 对 `ctx.projectPath` 的缺省兜底(豁免理由见守护表)。约定正本与两道机器可检的闸见 `src/cli/commands/CONTEXT.md`
|
|
27
27
|
|
|
28
28
|
## 注意事项
|
|
29
29
|
- 门禁系统不包含业务逻辑,只提供检查能力
|
|
30
30
|
- `acceptance.ts` 的 13 处 return 是 `AcceptanceGateResult`(无 gate/duration 字段,`evaluate()` 归一化为 GateResult 报告——设计如此,见该方法注释),不是 GateResult 字面量,故不套 `pass`/`fail` 构造器;改成 GateResult 会动公共类型形状
|
|
31
|
-
- 统一的是决策协议(id/order/三态),执行细节(gh pr view/正则黑名单/OpenAPI diff/
|
|
31
|
+
- 统一的是决策协议(id/order/三态),执行细节(gh pr view/正则黑名单/OpenAPI diff/json-summary 覆盖率)私有——不要把执行细节塞进 Gate 接口
|
|
32
|
+
- `PerformanceGate` 只执法有真实现的维度(coverage 走 json-summary、bundleSize 走 dist 测量);responseTime/memoryUsage/throughput 三维无真实现(原 Math.random 伪造 + 未接线的 runBenchmark)已随 benchmark 机制整体删除(ADR-0018,架构评审候选2)
|
|
32
33
|
- config.yml gates 段为新增面:引用未注册 id 直接抛错(无历史残留配置需兼容)
|
|
33
34
|
- CommandGate 为命令黑名单检查(SEC-006)
|
|
@@ -37,21 +37,16 @@ describe('PerformanceGate', () => {
|
|
|
37
37
|
const defaultGate = new PerformanceGate();
|
|
38
38
|
const config = defaultGate.getConfig();
|
|
39
39
|
expect(config.enabled).toBe(true);
|
|
40
|
-
expect(config.
|
|
41
|
-
expect(config.measureRuns).toBe(5);
|
|
40
|
+
expect(config.thresholds).toEqual({});
|
|
42
41
|
});
|
|
43
42
|
|
|
44
43
|
it('should accept custom config', () => {
|
|
45
44
|
const customGate = new PerformanceGate({
|
|
46
45
|
enabled: false,
|
|
47
|
-
warmupRuns: 3,
|
|
48
|
-
measureRuns: 10,
|
|
49
46
|
coverageTimeout: 60000,
|
|
50
47
|
});
|
|
51
48
|
const config = customGate.getConfig();
|
|
52
49
|
expect(config.enabled).toBe(false);
|
|
53
|
-
expect(config.warmupRuns).toBe(3);
|
|
54
|
-
expect(config.measureRuns).toBe(10);
|
|
55
50
|
expect(config.coverageTimeout).toBe(60000);
|
|
56
51
|
});
|
|
57
52
|
});
|
|
@@ -164,17 +159,16 @@ describe('PerformanceGate', () => {
|
|
|
164
159
|
|
|
165
160
|
it('should include metrics in result', async () => {
|
|
166
161
|
const strictGate = new PerformanceGate({
|
|
167
|
-
thresholds: {
|
|
168
|
-
maxResponseTime: 1000,
|
|
169
|
-
maxMemoryUsage: 500,
|
|
170
|
-
},
|
|
162
|
+
thresholds: { maxBundleSize: 1000 },
|
|
171
163
|
});
|
|
172
164
|
|
|
165
|
+
mockFs.readdir.mockResolvedValueOnce(['bundle.js'] as any);
|
|
166
|
+
mockFs.stat.mockResolvedValueOnce({ isFile: () => true, size: 500 * 1024 } as any);
|
|
167
|
+
|
|
173
168
|
const result = await strictGate.check(baseContext);
|
|
174
169
|
|
|
175
170
|
expect(result.details?.metrics).toBeDefined();
|
|
176
|
-
expect(result.details?.metrics.
|
|
177
|
-
expect(result.details?.metrics.memoryUsage).toBeDefined();
|
|
171
|
+
expect(result.details?.metrics.bundleSize).toBeDefined();
|
|
178
172
|
});
|
|
179
173
|
});
|
|
180
174
|
|
|
@@ -257,66 +251,6 @@ describe('PerformanceGate', () => {
|
|
|
257
251
|
});
|
|
258
252
|
});
|
|
259
253
|
|
|
260
|
-
describe('runBenchmark()', () => {
|
|
261
|
-
it('should return benchmark results', async () => {
|
|
262
|
-
mockExec.mockImplementation((cmd, opts, callback) => {
|
|
263
|
-
callback(null, { stdout: '', stderr: '' });
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const result = await gate.runBenchmark(baseContext);
|
|
267
|
-
|
|
268
|
-
expect(result.avgResponseTime).toBeGreaterThanOrEqual(0);
|
|
269
|
-
expect(result.avgMemoryUsage).toBeGreaterThanOrEqual(0);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should run warmup and measure runs', async () => {
|
|
273
|
-
const customGate = new PerformanceGate({
|
|
274
|
-
warmupRuns: 1,
|
|
275
|
-
measureRuns: 2,
|
|
276
|
-
benchmarkCommand: 'npm run bench',
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Each exec call should succeed
|
|
280
|
-
mockExec.mockImplementation((cmd, opts, callback) => {
|
|
281
|
-
callback(null, { stdout: '', stderr: '' });
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
await customGate.runBenchmark(baseContext);
|
|
285
|
-
|
|
286
|
-
// warmupRuns + measureRuns = 3 calls
|
|
287
|
-
expect(mockExec).toHaveBeenCalledTimes(3);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should use custom benchmark command', async () => {
|
|
291
|
-
const customGate = new PerformanceGate({
|
|
292
|
-
benchmarkCommand: 'npm run benchmark',
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
mockExec.mockImplementation((cmd, opts, callback) => {
|
|
296
|
-
expect(cmd).toContain('npm run benchmark');
|
|
297
|
-
callback(null, { stdout: '', stderr: '' });
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
await customGate.runBenchmark(baseContext);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should handle benchmark error', async () => {
|
|
304
|
-
const errorGate = new PerformanceGate({
|
|
305
|
-
warmupRuns: 1,
|
|
306
|
-
benchmarkCommand: 'npm run bench',
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// Mock exec to fail
|
|
310
|
-
mockExec.mockImplementationOnce((cmd, opts, callback) => {
|
|
311
|
-
callback(new Error('Benchmark failed'), null);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
const result = await errorGate.runBenchmark(baseContext);
|
|
315
|
-
|
|
316
|
-
expect(result.error).toContain('failed');
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
254
|
describe('setThresholds()', () => {
|
|
321
255
|
it('should update thresholds', () => {
|
|
322
256
|
gate.setThresholds({
|
|
@@ -346,12 +280,10 @@ describe('PerformanceGate', () => {
|
|
|
346
280
|
it('should update timeouts', () => {
|
|
347
281
|
gate.setTimeouts({
|
|
348
282
|
coverage: 30000,
|
|
349
|
-
benchmark: 20000,
|
|
350
283
|
});
|
|
351
284
|
|
|
352
285
|
const config = gate.getConfig();
|
|
353
286
|
expect(config.coverageTimeout).toBe(30000);
|
|
354
|
-
expect(config.benchmarkTimeout).toBe(20000);
|
|
355
287
|
});
|
|
356
288
|
});
|
|
357
289
|
|
package/src/gates/definitions.ts
CHANGED
|
@@ -111,8 +111,6 @@ export const GATE_DEFINITIONS: GateDefinition[] = [
|
|
|
111
111
|
{ flags: '--coverage-threshold <n>', description: '覆盖率阈值', defaultValue: '80' },
|
|
112
112
|
{ flags: '--bundle', description: '检查打包大小', defaultValue: false },
|
|
113
113
|
{ flags: '--bundle-threshold <n>', description: '打包大小阈值 (KB)', defaultValue: '500' },
|
|
114
|
-
{ flags: '--benchmark', description: '运行基准测试', defaultValue: false },
|
|
115
|
-
{ flags: '--benchmark-timeout <n>', description: '基准测试超时(秒)', defaultValue: '60' },
|
|
116
114
|
],
|
|
117
115
|
action: { module: 'performance', export: 'performance' },
|
|
118
116
|
mapActionArgs: (_positionals, options) => [
|
|
@@ -122,8 +120,6 @@ export const GATE_DEFINITIONS: GateDefinition[] = [
|
|
|
122
120
|
coverageThreshold: parseInt(String(options.coverageThreshold), 10),
|
|
123
121
|
bundle: options.bundle,
|
|
124
122
|
bundleThreshold: parseInt(String(options.bundleThreshold), 10),
|
|
125
|
-
benchmark: options.benchmark,
|
|
126
|
-
benchmarkTimeout: parseInt(String(options.benchmarkTimeout), 10),
|
|
127
123
|
},
|
|
128
124
|
],
|
|
129
125
|
},
|
package/src/gates/performance.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 性能门禁
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 检查性能指标(架构评审候选2:只留有真实现的维度):
|
|
5
|
+
* - 测试覆盖率(json-summary)
|
|
6
|
+
* - 打包大小(dist 目录测量)
|
|
7
|
+
*
|
|
8
|
+
* 响应时间/内存使用/吞吐量维度已删除:原实现用 Math.random() 伪造指标并据此判负,
|
|
9
|
+
* runBenchmark 的真实现从未被 check() 调用且语义可疑(量的是 harness 进程自己的
|
|
10
|
+
* heap)——零真实消费者的维度按 ADR-0015 同构判例删薄(ADR-0018)。
|
|
11
|
+
*
|
|
10
12
|
* 改进:
|
|
11
13
|
* - 添加超时机制
|
|
12
14
|
* - 改进错误处理
|
|
@@ -23,8 +25,6 @@ import { decisionFromResult } from './decision';
|
|
|
23
25
|
// 默认超时时间(毫秒)
|
|
24
26
|
const DEFAULT_TIMEOUTS = {
|
|
25
27
|
coverage: 120000, // 覆盖率测试:2分钟
|
|
26
|
-
benchmark: 60000, // 基准测试:1分钟
|
|
27
|
-
bundle: 10000, // 打包大小:10秒
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -33,8 +33,6 @@ const DEFAULT_TIMEOUTS = {
|
|
|
33
33
|
export interface ExtendedPerformanceGateConfig extends PerformanceGateConfig {
|
|
34
34
|
/** 覆盖率测试超时(毫秒) */
|
|
35
35
|
coverageTimeout?: number;
|
|
36
|
-
/** 基准测试超时(毫秒) */
|
|
37
|
-
benchmarkTimeout?: number;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
/**
|
|
@@ -48,12 +46,8 @@ export class PerformanceGate implements Gate {
|
|
|
48
46
|
constructor(config: Partial<ExtendedPerformanceGateConfig> = {}) {
|
|
49
47
|
this.config = {
|
|
50
48
|
enabled: config.enabled ?? true,
|
|
51
|
-
benchmarkCommand: config.benchmarkCommand ?? '',
|
|
52
49
|
thresholds: config.thresholds ?? {},
|
|
53
|
-
warmupRuns: config.warmupRuns ?? 2,
|
|
54
|
-
measureRuns: config.measureRuns ?? 5,
|
|
55
50
|
coverageTimeout: config.coverageTimeout ?? DEFAULT_TIMEOUTS.coverage,
|
|
56
|
-
benchmarkTimeout: config.benchmarkTimeout ?? DEFAULT_TIMEOUTS.benchmark,
|
|
57
51
|
};
|
|
58
52
|
}
|
|
59
53
|
|
|
@@ -80,20 +74,6 @@ export class PerformanceGate implements Gate {
|
|
|
80
74
|
const failures: string[] = [];
|
|
81
75
|
const warnings: string[] = [];
|
|
82
76
|
|
|
83
|
-
// 检查响应时间
|
|
84
|
-
if (thresholds.maxResponseTime && metrics.responseTime !== undefined) {
|
|
85
|
-
if (metrics.responseTime > thresholds.maxResponseTime) {
|
|
86
|
-
failures.push(`响应时间 ${metrics.responseTime}ms > ${thresholds.maxResponseTime}ms`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 检查内存使用
|
|
91
|
-
if (thresholds.maxMemoryUsage && metrics.memoryUsage !== undefined) {
|
|
92
|
-
if (metrics.memoryUsage > thresholds.maxMemoryUsage) {
|
|
93
|
-
failures.push(`内存使用 ${metrics.memoryUsage}MB > ${thresholds.maxMemoryUsage}MB`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
77
|
// 检查覆盖率
|
|
98
78
|
if (thresholds.minCoverage) {
|
|
99
79
|
if (metrics.coverageError) {
|
|
@@ -124,8 +104,6 @@ export class PerformanceGate implements Gate {
|
|
|
124
104
|
startTime,
|
|
125
105
|
{
|
|
126
106
|
metrics: {
|
|
127
|
-
responseTime: metrics.responseTime,
|
|
128
|
-
memoryUsage: metrics.memoryUsage,
|
|
129
107
|
coverage: metrics.coverage,
|
|
130
108
|
bundleSize: metrics.bundleSize,
|
|
131
109
|
},
|
|
@@ -146,8 +124,6 @@ export class PerformanceGate implements Gate {
|
|
|
146
124
|
projectPath: string,
|
|
147
125
|
thresholds: PerformanceThresholds
|
|
148
126
|
): Promise<{
|
|
149
|
-
responseTime?: number;
|
|
150
|
-
memoryUsage?: number;
|
|
151
127
|
coverage?: number;
|
|
152
128
|
coverageError?: string;
|
|
153
129
|
bundleSize?: number;
|
|
@@ -175,15 +151,6 @@ export class PerformanceGate implements Gate {
|
|
|
175
151
|
}
|
|
176
152
|
}
|
|
177
153
|
|
|
178
|
-
// 模拟响应时间和内存(需要实际基准测试)
|
|
179
|
-
if (thresholds.maxResponseTime) {
|
|
180
|
-
metrics.responseTime = Math.floor(Math.random() * 500) + 100;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (thresholds.maxMemoryUsage) {
|
|
184
|
-
metrics.memoryUsage = Math.floor(Math.random() * 200) + 50;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
154
|
return metrics;
|
|
188
155
|
}
|
|
189
156
|
|
|
@@ -258,78 +225,11 @@ export class PerformanceGate implements Gate {
|
|
|
258
225
|
*/
|
|
259
226
|
private formatMetrics(metrics: any): string {
|
|
260
227
|
const parts: string[] = [];
|
|
261
|
-
if (metrics.responseTime !== undefined) parts.push(`响应时间=${metrics.responseTime}ms`);
|
|
262
|
-
if (metrics.memoryUsage !== undefined) parts.push(`内存=${metrics.memoryUsage}MB`);
|
|
263
228
|
if (metrics.coverage !== undefined) parts.push(`覆盖率=${metrics.coverage}%`);
|
|
264
229
|
if (metrics.bundleSize !== undefined) parts.push(`打包=${metrics.bundleSize}KB`);
|
|
265
230
|
return parts.join(', ') || '无指标';
|
|
266
231
|
}
|
|
267
232
|
|
|
268
|
-
/**
|
|
269
|
-
* 运行基准测试(带超时)
|
|
270
|
-
*/
|
|
271
|
-
async runBenchmark(context: GateContext): Promise<{
|
|
272
|
-
avgResponseTime: number;
|
|
273
|
-
avgMemoryUsage: number;
|
|
274
|
-
minResponseTime: number;
|
|
275
|
-
maxResponseTime: number;
|
|
276
|
-
error?: string;
|
|
277
|
-
}> {
|
|
278
|
-
const results: Array<{ responseTime: number; memoryUsage: number }> = [];
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
// 预热运行
|
|
282
|
-
for (let i = 0; i < this.config.warmupRuns; i++) {
|
|
283
|
-
await this.singleBenchmark(context);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 测量运行
|
|
287
|
-
for (let i = 0; i < this.config.measureRuns; i++) {
|
|
288
|
-
const result = await this.singleBenchmark(context);
|
|
289
|
-
results.push(result);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const responseTimes = results.map(r => r.responseTime);
|
|
293
|
-
const memoryUsages = results.map(r => r.memoryUsage);
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
avgResponseTime: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
|
|
297
|
-
avgMemoryUsage: memoryUsages.reduce((a, b) => a + b, 0) / memoryUsages.length,
|
|
298
|
-
minResponseTime: Math.min(...responseTimes),
|
|
299
|
-
maxResponseTime: Math.max(...responseTimes),
|
|
300
|
-
};
|
|
301
|
-
} catch (error: any) {
|
|
302
|
-
return {
|
|
303
|
-
avgResponseTime: 0,
|
|
304
|
-
avgMemoryUsage: 0,
|
|
305
|
-
minResponseTime: 0,
|
|
306
|
-
maxResponseTime: 0,
|
|
307
|
-
error: error.message || '基准测试失败',
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* 单次基准测试(带超时)
|
|
314
|
-
*/
|
|
315
|
-
private async singleBenchmark(context: GateContext): Promise<{ responseTime: number; memoryUsage: number }> {
|
|
316
|
-
const start = Date.now();
|
|
317
|
-
|
|
318
|
-
// 如果有基准测试命令,运行它
|
|
319
|
-
if (this.config.benchmarkCommand || context.benchmarkCommand) {
|
|
320
|
-
await execAsync(this.config.benchmarkCommand || context.benchmarkCommand!, {
|
|
321
|
-
cwd: context.projectPath,
|
|
322
|
-
timeout: this.config.benchmarkTimeout,
|
|
323
|
-
killSignal: 'SIGTERM',
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const responseTime = Date.now() - start;
|
|
328
|
-
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
329
|
-
|
|
330
|
-
return { responseTime, memoryUsage };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
233
|
/**
|
|
334
234
|
* 设置阈值
|
|
335
235
|
*/
|
|
@@ -342,10 +242,8 @@ export class PerformanceGate implements Gate {
|
|
|
342
242
|
*/
|
|
343
243
|
setTimeouts(options: {
|
|
344
244
|
coverage?: number;
|
|
345
|
-
benchmark?: number;
|
|
346
245
|
}): void {
|
|
347
246
|
if (options.coverage) this.config.coverageTimeout = options.coverage;
|
|
348
|
-
if (options.benchmark) this.config.benchmarkTimeout = options.benchmark;
|
|
349
247
|
}
|
|
350
248
|
|
|
351
249
|
/**
|
package/src/gates/types.ts
CHANGED
|
@@ -133,7 +133,6 @@ export interface GateContext {
|
|
|
133
133
|
|
|
134
134
|
// Performance Gate
|
|
135
135
|
performanceThresholds?: PerformanceThresholds;
|
|
136
|
-
benchmarkCommand?: string;
|
|
137
136
|
|
|
138
137
|
// Contract Gate
|
|
139
138
|
oldContractPath?: string;
|
|
@@ -151,14 +150,12 @@ export interface GateContext {
|
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
/**
|
|
154
|
-
*
|
|
153
|
+
* 性能阈值(架构评审候选2:只留有真实现的维度——coverage 走 json-summary、
|
|
154
|
+
* bundleSize 走 dist 目录测量;responseTime/memoryUsage/throughput 无真实现已删)
|
|
155
155
|
*/
|
|
156
156
|
export interface PerformanceThresholds {
|
|
157
|
-
maxResponseTime?: number; // ms
|
|
158
|
-
maxMemoryUsage?: number; // MB
|
|
159
157
|
minCoverage?: number; // %
|
|
160
158
|
maxBundleSize?: number; // KB
|
|
161
|
-
minThroughput?: number; // req/s
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
/**
|
|
@@ -187,10 +184,7 @@ export interface SecurityGateConfig {
|
|
|
187
184
|
*/
|
|
188
185
|
export interface PerformanceGateConfig {
|
|
189
186
|
enabled: boolean;
|
|
190
|
-
benchmarkCommand?: string;
|
|
191
187
|
thresholds: PerformanceThresholds;
|
|
192
|
-
warmupRuns: number;
|
|
193
|
-
measureRuns: number;
|
|
194
188
|
}
|
|
195
189
|
|
|
196
190
|
/**
|