@dommaker/harness 1.4.0 → 1.6.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 +23 -0
- package/README.md +0 -2
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +28 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +1 -39
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +0 -19
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +0 -105
- package/dist/cli/commands/knowledge.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 +27 -5
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts +17 -6
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +48 -19
- package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js +15 -6
- package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +2 -2
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +2 -1
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/types.d.ts +41 -2
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js +33 -0
- package/dist/core/constraints/checkers/types.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 +0 -125
- 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/checker-gate.d.ts +2 -1
- package/dist/gates/checker-gate.d.ts.map +1 -1
- package/dist/gates/checker-gate.js +10 -7
- package/dist/gates/checker-gate.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/constraint.d.ts +8 -0
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +6 -1
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/passes-gate.d.ts +0 -19
- package/dist/types/passes-gate.d.ts.map +1 -1
- package/dist/types/trace.d.ts +9 -0
- package/dist/types/trace.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -0
- package/src/__tests__/passes-gate-check.test.ts +20 -4
- package/src/__tests__/passes-gate.test.ts +18 -119
- package/src/__tests__/performance-gate.test.ts +3 -11
- package/src/__tests__/public-exports.test.ts +46 -16
- package/src/__tests__/trace-analyzer.test.ts +52 -2
- package/src/cli/commands/CONTEXT.md +5 -4
- package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +6 -0
- package/src/cli/commands/__tests__/check.test.ts +74 -0
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +70 -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__/project-path-convention.test.ts +85 -23
- package/src/cli/commands/__tests__/registry.test.ts +52 -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/check.ts +39 -1
- package/src/cli/commands/definitions.ts +1 -39
- package/src/cli/commands/knowledge.ts +0 -119
- 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 +6 -6
- package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
- package/src/core/constraints/checker.ts +41 -6
- package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +90 -31
- package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +176 -0
- package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +18 -9
- package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +12 -4
- package/src/core/constraints/checkers/capability-sync.ts +56 -18
- package/src/core/constraints/checkers/docs-freshness.ts +18 -5
- package/src/core/constraints/checkers/index.ts +9 -2
- package/src/core/constraints/checkers/types.ts +58 -2
- package/src/core/constraints/git-evidence.ts +6 -3
- package/src/core/index.ts +0 -11
- 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 -1
- package/src/core/validators/passes-gate.ts +1 -149
- package/src/failure/CONTEXT.md +8 -6
- package/src/failure/index.ts +0 -10
- package/src/gates/CONTEXT.md +4 -3
- package/src/gates/__tests__/checker-gate.test.ts +24 -0
- package/src/gates/__tests__/performance.test.ts +6 -74
- package/src/gates/checker-gate.ts +11 -15
- 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 -36
- 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/constraint.ts +16 -2
- package/src/types/passes-gate.ts +0 -26
- package/src/types/trace.ts +10 -0
- 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 -25
- package/dist/cli/commands/update-user-model.d.ts.map +0 -1
- package/dist/cli/commands/update-user-model.js +0 -397
- 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 -234
- package/src/cli/commands/analyze-sessions.ts +0 -335
- package/src/cli/commands/update-user-model.ts +0 -461
- 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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* status 命令补充测试
|
|
3
|
+
*
|
|
4
|
+
* ADR-0020 起不再 mock TraceAnalyzer/TraceCollector:异常判定走真实纯函数,
|
|
5
|
+
* 用例喂能真的判出异常的 trace(passRate < 0.3 → low_pass_rate)。
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { status } from '../status';
|
|
6
9
|
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
7
10
|
import * as fs from 'fs';
|
|
8
|
-
import {
|
|
11
|
+
import type { ExecutionTrace } from '../../../types/trace';
|
|
9
12
|
|
|
10
13
|
// Mock fs
|
|
11
14
|
jest.mock('fs', () => ({
|
|
@@ -15,22 +18,6 @@ jest.mock('fs', () => ({
|
|
|
15
18
|
writeFileSync: jest.fn(),
|
|
16
19
|
}));
|
|
17
20
|
|
|
18
|
-
// Mock TraceCollector
|
|
19
|
-
jest.mock('../../../monitoring/traces', () => ({
|
|
20
|
-
TraceCollector: jest.fn().mockImplementation(() => ({
|
|
21
|
-
record: jest.fn(),
|
|
22
|
-
read: jest.fn().mockReturnValue([]),
|
|
23
|
-
})),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
// Mock TraceAnalyzer
|
|
27
|
-
jest.mock('../../../monitoring/trace-analyzer', () => ({
|
|
28
|
-
TraceAnalyzer: jest.fn().mockImplementation(() => ({
|
|
29
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
30
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
31
|
-
})),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
21
|
// Mock chalk
|
|
35
22
|
jest.mock('chalk', () => ({
|
|
36
23
|
blue: jest.fn((str: string) => str),
|
|
@@ -41,7 +28,18 @@ jest.mock('chalk', () => ({
|
|
|
41
28
|
}));
|
|
42
29
|
|
|
43
30
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
44
|
-
|
|
31
|
+
|
|
32
|
+
/** trace 文件内容:一 trace 一行,缺省字段补齐成合法 ExecutionTrace(timestamp 递增) */
|
|
33
|
+
function traceFile(traces: Array<Partial<ExecutionTrace>>): string {
|
|
34
|
+
return traces
|
|
35
|
+
.map((t, i) => JSON.stringify({ level: 'iron_law', timestamp: i + 1, result: 'pass', ...t }))
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** n 条同一约束的 pass/fail 记录 */
|
|
40
|
+
function repeats(constraintId: string, n: number, result: ExecutionTrace['result']): Array<Partial<ExecutionTrace>> {
|
|
41
|
+
return Array.from({ length: n }, () => ({ constraintId, result }));
|
|
42
|
+
}
|
|
45
43
|
|
|
46
44
|
let io: CapturingIO;
|
|
47
45
|
beforeEach(() => {
|
|
@@ -60,140 +58,89 @@ describe('status command - 补充覆盖', () => {
|
|
|
60
58
|
|
|
61
59
|
describe('异常检测完整流程', () => {
|
|
62
60
|
it('有异常时应该显示详细信息和下一步建议', async () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const mockAnomalies = [
|
|
69
|
-
{
|
|
70
|
-
constraintId: 'no_bypass_checkpoint',
|
|
71
|
-
type: 'high_failure_rate',
|
|
72
|
-
current: 0.8,
|
|
73
|
-
threshold: 0.5,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
constraintId: 'no_completion_without_verification',
|
|
77
|
-
type: 'frequent_bypass',
|
|
78
|
-
current: 0.3,
|
|
79
|
-
threshold: 0.1,
|
|
80
|
-
},
|
|
81
|
-
];
|
|
82
|
-
const mockAnalyze = {
|
|
83
|
-
summarize: jest.fn().mockReturnValue([
|
|
84
|
-
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 0.2, failRate: 0.8, totalChecks: 10 },
|
|
85
|
-
]),
|
|
86
|
-
detectAnomalies: jest.fn().mockReturnValue(mockAnomalies),
|
|
87
|
-
};
|
|
88
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
61
|
+
// 两个约束各自全失败 → 各自判出一枚 low_pass_rate
|
|
62
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
63
|
+
...repeats('no_bypass_checkpoint', 2, 'fail'),
|
|
64
|
+
...repeats('no_completion_without_verification', 2, 'fail'),
|
|
65
|
+
]));
|
|
89
66
|
|
|
90
67
|
await status({ anomalies: true }, io);
|
|
91
|
-
|
|
68
|
+
|
|
92
69
|
const output = io.outText();
|
|
93
|
-
expect(output).toContain('
|
|
94
|
-
expect(
|
|
70
|
+
expect(output).toContain('发现 2 个异常');
|
|
71
|
+
expect(output).toContain('no_completion_without_verification');
|
|
72
|
+
expect(output).toContain('类型: low_pass_rate');
|
|
73
|
+
expect(output).toContain('下一步建议');
|
|
95
74
|
});
|
|
96
75
|
|
|
97
76
|
it('无异常时应该显示未发现异常', async () => {
|
|
98
|
-
mockFs.readFileSync.mockReturnValue(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
102
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
103
|
-
};
|
|
104
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
77
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
78
|
+
{ constraintId: 'test', result: 'pass' },
|
|
79
|
+
]));
|
|
105
80
|
|
|
106
81
|
await status({ anomalies: true }, io);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(output).toContain('未发现异常');
|
|
82
|
+
|
|
83
|
+
expect(io.outText()).toContain('✅ 未发现异常');
|
|
110
84
|
});
|
|
111
85
|
});
|
|
112
86
|
|
|
113
87
|
describe('约束级别统计', () => {
|
|
114
88
|
it('应该显示 Iron Laws 统计', async () => {
|
|
115
|
-
|
|
116
|
-
{ constraintId: 'no_bypass_checkpoint'
|
|
117
|
-
{ constraintId: 'no_completion_without_verification'
|
|
118
|
-
];
|
|
119
|
-
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
120
|
-
|
|
121
|
-
const mockSummaries = [
|
|
122
|
-
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, failRate: 0, totalChecks: 10 },
|
|
123
|
-
{ constraintId: 'no_completion_without_verification', level: 'iron_law', passRate: 0.8, failRate: 0.2, totalChecks: 5 },
|
|
124
|
-
];
|
|
125
|
-
const mockAnalyze = {
|
|
126
|
-
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
127
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
128
|
-
};
|
|
129
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
89
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
90
|
+
{ constraintId: 'no_bypass_checkpoint' },
|
|
91
|
+
{ constraintId: 'no_completion_without_verification' },
|
|
92
|
+
]));
|
|
130
93
|
|
|
131
94
|
await status({}, io);
|
|
132
|
-
|
|
95
|
+
|
|
133
96
|
const output = io.outText();
|
|
134
|
-
expect(output).toContain('
|
|
97
|
+
expect(output).toContain('📈 约束统计:');
|
|
98
|
+
expect(output).toContain('🔴 Iron Laws:');
|
|
99
|
+
expect(output).toContain('✅ no_completion_without_verification');
|
|
135
100
|
});
|
|
136
101
|
|
|
137
102
|
it('应该显示 Guidelines 统计', async () => {
|
|
138
|
-
|
|
103
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
139
104
|
{ constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
|
|
140
|
-
];
|
|
141
|
-
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
142
|
-
|
|
143
|
-
const mockSummaries = [
|
|
144
|
-
{ constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, failRate: 0.1, totalChecks: 20 },
|
|
145
|
-
];
|
|
146
|
-
const mockAnalyze = {
|
|
147
|
-
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
148
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
149
|
-
};
|
|
150
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
105
|
+
]));
|
|
151
106
|
|
|
152
107
|
await status({}, io);
|
|
153
|
-
|
|
154
|
-
expect(io.outText()).
|
|
108
|
+
|
|
109
|
+
expect(io.outText()).toContain('🟡 Guidelines:');
|
|
155
110
|
});
|
|
156
111
|
});
|
|
157
112
|
|
|
158
113
|
describe('详细模式扩展', () => {
|
|
159
114
|
it('详细模式应该显示所有级别详情', async () => {
|
|
160
|
-
|
|
161
|
-
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law'
|
|
162
|
-
{ constraintId: 'capability_sync', level: 'guideline'
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const mockSummaries = [
|
|
167
|
-
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, failRate: 0, totalChecks: 10 },
|
|
168
|
-
{ constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, failRate: 0.1, totalChecks: 20 },
|
|
169
|
-
];
|
|
170
|
-
const mockAnalyze = {
|
|
171
|
-
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
172
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
173
|
-
};
|
|
174
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
115
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
116
|
+
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law' },
|
|
117
|
+
{ constraintId: 'capability_sync', level: 'guideline' },
|
|
118
|
+
{ constraintId: 'capability_sync', level: 'guideline' },
|
|
119
|
+
]));
|
|
175
120
|
|
|
176
121
|
await status({ detail: true }, io);
|
|
177
|
-
|
|
122
|
+
|
|
178
123
|
const output = io.outText();
|
|
179
|
-
expect(output).toContain('
|
|
124
|
+
expect(output).toContain('📈 约束统计:');
|
|
125
|
+
expect(output).toContain('🔴 Iron Laws:');
|
|
126
|
+
expect(output).toContain('🟡 Guidelines:');
|
|
127
|
+
expect(output).toContain('检查: 2 | 通过: 100% | 失败: 0%');
|
|
180
128
|
});
|
|
181
129
|
});
|
|
182
130
|
|
|
183
131
|
describe('JSON parse 异常处理', () => {
|
|
184
132
|
it('无效 JSON 行应该被过滤', async () => {
|
|
185
|
-
mockFs.readFileSync.mockReturnValue(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
189
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
190
|
-
};
|
|
191
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
133
|
+
mockFs.readFileSync.mockReturnValue(
|
|
134
|
+
['invalid json', JSON.stringify({ constraintId: 'valid', level: 'iron_law', timestamp: 1, result: 'pass' }), 'also invalid'].join('\n')
|
|
135
|
+
);
|
|
192
136
|
|
|
193
137
|
await status({}, io);
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
138
|
+
|
|
139
|
+
// 应该成功处理,不会抛出异常:坏行进 stderr 告知,合法行进统计
|
|
140
|
+
const output = io.outText();
|
|
141
|
+
expect(output).toContain('记录数: 3 条');
|
|
142
|
+
expect(output).toContain('✅ valid');
|
|
143
|
+
expect(io.errText()).toContain('2 行损坏');
|
|
197
144
|
});
|
|
198
145
|
});
|
|
199
|
-
});
|
|
146
|
+
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* status 命令测试
|
|
3
|
+
*
|
|
4
|
+
* ADR-0020 起不再 mock TraceAnalyzer/TraceCollector:统计与异常由 trace-analyzer
|
|
5
|
+
* 的模块级纯函数真算,用例喂合法 trace 行、断言真实输出。
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { status } from '../status';
|
|
6
9
|
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
7
10
|
import * as fs from 'fs';
|
|
8
|
-
import {
|
|
11
|
+
import type { ExecutionTrace } from '../../../types/trace';
|
|
9
12
|
|
|
10
13
|
// Mock fs
|
|
11
14
|
jest.mock('fs', () => ({
|
|
@@ -15,22 +18,6 @@ jest.mock('fs', () => ({
|
|
|
15
18
|
writeFileSync: jest.fn(),
|
|
16
19
|
}));
|
|
17
20
|
|
|
18
|
-
// Mock TraceCollector
|
|
19
|
-
jest.mock('../../../monitoring/traces', () => ({
|
|
20
|
-
TraceCollector: jest.fn().mockImplementation(() => ({
|
|
21
|
-
record: jest.fn(),
|
|
22
|
-
read: jest.fn().mockReturnValue([]),
|
|
23
|
-
})),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
// Mock TraceAnalyzer
|
|
27
|
-
jest.mock('../../../monitoring/trace-analyzer', () => ({
|
|
28
|
-
TraceAnalyzer: jest.fn().mockImplementation(() => ({
|
|
29
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
30
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
31
|
-
})),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
21
|
// Mock chalk
|
|
35
22
|
jest.mock('chalk', () => ({
|
|
36
23
|
blue: jest.fn((str: string) => str),
|
|
@@ -41,7 +28,18 @@ jest.mock('chalk', () => ({
|
|
|
41
28
|
}));
|
|
42
29
|
|
|
43
30
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
44
|
-
|
|
31
|
+
|
|
32
|
+
/** trace 文件内容:一 trace 一行,缺省字段补齐成合法 ExecutionTrace(timestamp 递增) */
|
|
33
|
+
function traceFile(traces: Array<Partial<ExecutionTrace>>): string {
|
|
34
|
+
return traces
|
|
35
|
+
.map((t, i) => JSON.stringify({ level: 'iron_law', timestamp: i + 1, result: 'pass', ...t }))
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** n 条同一约束的 pass/fail 记录 */
|
|
40
|
+
function repeats(constraintId: string, n: number, result: ExecutionTrace['result']): Array<Partial<ExecutionTrace>> {
|
|
41
|
+
return Array.from({ length: n }, () => ({ constraintId, result }));
|
|
42
|
+
}
|
|
45
43
|
|
|
46
44
|
let io: CapturingIO;
|
|
47
45
|
beforeEach(() => {
|
|
@@ -80,98 +78,56 @@ describe('status command', () => {
|
|
|
80
78
|
|
|
81
79
|
describe('正常状态显示', () => {
|
|
82
80
|
it('应该显示记录数', async () => {
|
|
83
|
-
|
|
84
|
-
{ constraintId: 'test1'
|
|
85
|
-
{ constraintId: 'test2', level: 'guideline'
|
|
86
|
-
];
|
|
87
|
-
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
88
|
-
|
|
89
|
-
const mockSummarize = jest.fn().mockReturnValue([
|
|
90
|
-
{ constraintId: 'test1', level: 'iron_law', passRate: 1, failRate: 0, totalChecks: 1 },
|
|
91
|
-
]);
|
|
92
|
-
const mockAnalyze = {
|
|
93
|
-
summarize: mockSummarize,
|
|
94
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
95
|
-
};
|
|
96
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
81
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
82
|
+
{ constraintId: 'test1' },
|
|
83
|
+
{ constraintId: 'test2', level: 'guideline' },
|
|
84
|
+
]));
|
|
97
85
|
|
|
98
86
|
await status({}, io);
|
|
99
|
-
expect(io.outText()).
|
|
87
|
+
expect(io.outText()).toContain('记录数: 2 条');
|
|
88
|
+
expect(io.outText()).toContain('📈 约束统计:');
|
|
100
89
|
});
|
|
101
90
|
|
|
102
91
|
it('应该显示 Iron Laws 统计', async () => {
|
|
103
|
-
|
|
104
|
-
{ constraintId: 'no_bypass_checkpoint'
|
|
105
|
-
];
|
|
106
|
-
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
107
|
-
|
|
108
|
-
const mockSummarize = jest.fn().mockReturnValue([
|
|
109
|
-
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, failRate: 0, totalChecks: 5 },
|
|
110
|
-
]);
|
|
111
|
-
const mockAnalyze = {
|
|
112
|
-
summarize: mockSummarize,
|
|
113
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
114
|
-
};
|
|
115
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
92
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
93
|
+
{ constraintId: 'no_bypass_checkpoint' },
|
|
94
|
+
]));
|
|
116
95
|
|
|
117
96
|
await status({ detail: true }, io);
|
|
118
|
-
expect(io.outText()).
|
|
97
|
+
expect(io.outText()).toContain('🔴 Iron Laws:');
|
|
98
|
+
expect(io.outText()).toContain('✅ no_bypass_checkpoint');
|
|
99
|
+
expect(io.outText()).toContain('检查: 1 | 通过: 100% | 失败: 0%');
|
|
119
100
|
});
|
|
120
101
|
});
|
|
121
102
|
|
|
122
103
|
describe('异常检测', () => {
|
|
123
104
|
it('应该显示异常列表', async () => {
|
|
124
|
-
|
|
125
|
-
{ constraintId: 'test1',
|
|
126
|
-
];
|
|
127
|
-
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
128
|
-
|
|
129
|
-
const mockAnomalies = [
|
|
130
|
-
{ constraintId: 'test1', type: 'high_failure_rate', current: 0.8, threshold: 0.5 },
|
|
131
|
-
];
|
|
132
|
-
const mockAnalyze = {
|
|
133
|
-
summarize: jest.fn().mockReturnValue([
|
|
134
|
-
{ constraintId: 'test1', level: 'iron_law', passRate: 0.2, failRate: 0.8, totalChecks: 5 },
|
|
135
|
-
]),
|
|
136
|
-
detectAnomalies: jest.fn().mockReturnValue(mockAnomalies),
|
|
137
|
-
};
|
|
138
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
105
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
106
|
+
{ constraintId: 'test1', result: 'fail' },
|
|
107
|
+
]));
|
|
139
108
|
|
|
140
109
|
await status({ anomalies: true }, io);
|
|
141
|
-
expect(io.outText()).toContain('
|
|
110
|
+
expect(io.outText()).toContain('发现 1 个异常');
|
|
111
|
+
expect(io.outText()).toContain(' test1');
|
|
112
|
+
expect(io.outText()).toContain('类型: low_pass_rate');
|
|
142
113
|
});
|
|
143
114
|
});
|
|
144
115
|
|
|
145
116
|
describe('详细模式', () => {
|
|
146
117
|
it('应该显示详细统计', async () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const mockSummarize = jest.fn().mockReturnValue([
|
|
153
|
-
{ constraintId: 'test1', level: 'iron_law', passRate: 0.8, failRate: 0.2, totalChecks: 10 },
|
|
154
|
-
]);
|
|
155
|
-
const mockAnalyze = {
|
|
156
|
-
summarize: mockSummarize,
|
|
157
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
158
|
-
};
|
|
159
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
118
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
119
|
+
...repeats('test1', 4, 'pass'),
|
|
120
|
+
{ constraintId: 'test1', result: 'fail' },
|
|
121
|
+
]));
|
|
160
122
|
|
|
161
123
|
await status({ detail: true }, io);
|
|
162
|
-
expect(io.outText()).
|
|
124
|
+
expect(io.outText()).toContain('检查: 5 | 通过: 80% | 失败: 20%');
|
|
163
125
|
});
|
|
164
126
|
});
|
|
165
127
|
|
|
166
128
|
describe('状态文件更新', () => {
|
|
167
129
|
it('应该更新 .state.json', async () => {
|
|
168
|
-
mockFs.readFileSync.mockReturnValue(
|
|
169
|
-
|
|
170
|
-
const mockAnalyze = {
|
|
171
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
172
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
173
|
-
};
|
|
174
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
130
|
+
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
175
131
|
|
|
176
132
|
await status({}, io);
|
|
177
133
|
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
@@ -180,110 +136,72 @@ describe('status command', () => {
|
|
|
180
136
|
|
|
181
137
|
describe('Guidelines 统计', () => {
|
|
182
138
|
it('应该显示 Guidelines 统计', async () => {
|
|
183
|
-
mockFs.readFileSync.mockReturnValue(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
summarize: jest.fn().mockReturnValue([
|
|
187
|
-
{ constraintId: 'test_guide', level: 'guideline', passRate: 0.9, failRate: 0.1, totalChecks: 10 },
|
|
188
|
-
]),
|
|
189
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
190
|
-
};
|
|
191
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
139
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
140
|
+
{ constraintId: 'test_guide', level: 'guideline' },
|
|
141
|
+
]));
|
|
192
142
|
|
|
193
143
|
await status({}, io);
|
|
194
|
-
expect(io.outText()).toContain('Guidelines');
|
|
144
|
+
expect(io.outText()).toContain('🟡 Guidelines:');
|
|
145
|
+
expect(io.outText()).toContain('✅ test_guide');
|
|
195
146
|
});
|
|
196
147
|
});
|
|
197
148
|
|
|
198
149
|
describe('异常模式', () => {
|
|
199
150
|
it('应该显示未发现异常当无异常', async () => {
|
|
200
|
-
mockFs.readFileSync.mockReturnValue(
|
|
201
|
-
|
|
202
|
-
const mockAnalyze = {
|
|
203
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
204
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
205
|
-
};
|
|
206
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
151
|
+
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
207
152
|
|
|
208
153
|
await status({ anomalies: true }, io);
|
|
209
|
-
expect(io.outText()).toContain('未发现异常');
|
|
154
|
+
expect(io.outText()).toContain('✅ 未发现异常');
|
|
210
155
|
});
|
|
211
156
|
|
|
212
157
|
it('应该显示异常详情', async () => {
|
|
213
|
-
mockFs.readFileSync.mockReturnValue(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
217
|
-
detectAnomalies: jest.fn().mockReturnValue([
|
|
218
|
-
{ constraintId: 'test', type: 'high_failure_rate', data: { currentRate: 0.8, threshold: 0.5 } },
|
|
219
|
-
]),
|
|
220
|
-
};
|
|
221
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
158
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
159
|
+
{ constraintId: 'test', result: 'fail' },
|
|
160
|
+
]));
|
|
222
161
|
|
|
223
162
|
await status({ anomalies: true }, io);
|
|
224
|
-
expect(io.outText()).toContain('
|
|
163
|
+
expect(io.outText()).toContain('当前值: 0');
|
|
164
|
+
expect(io.outText()).toContain('阈值: 0.3');
|
|
225
165
|
expect(io.outText()).toContain('下一步建议');
|
|
226
166
|
});
|
|
227
167
|
});
|
|
228
168
|
|
|
229
169
|
describe('建议', () => {
|
|
230
170
|
it('应该显示良好建议当 trace >= 100', async () => {
|
|
231
|
-
|
|
232
|
-
mockFs.readFileSync.mockReturnValue(traces);
|
|
233
|
-
|
|
234
|
-
const mockAnalyze = {
|
|
235
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
236
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
237
|
-
};
|
|
238
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
171
|
+
mockFs.readFileSync.mockReturnValue(traceFile(repeats('test', 100, 'pass')));
|
|
239
172
|
|
|
240
173
|
await status({}, io);
|
|
174
|
+
expect(io.outText()).toContain('记录数: 100 条');
|
|
241
175
|
expect(io.outText()).toContain('状态良好');
|
|
242
176
|
});
|
|
243
177
|
|
|
244
178
|
it('应该显示积累数据建议当 trace < 100', async () => {
|
|
245
|
-
mockFs.readFileSync.mockReturnValue(
|
|
246
|
-
|
|
247
|
-
const mockAnalyze = {
|
|
248
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
249
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
250
|
-
};
|
|
251
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
179
|
+
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
252
180
|
|
|
253
181
|
await status({}, io);
|
|
254
182
|
expect(io.outText()).toContain('继续积累数据');
|
|
255
183
|
});
|
|
256
184
|
|
|
257
185
|
it('应该显示诊断建议当有异常', async () => {
|
|
258
|
-
mockFs.readFileSync.mockReturnValue(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
summarize: jest.fn().mockReturnValue([]),
|
|
262
|
-
detectAnomalies: jest.fn().mockReturnValue([
|
|
263
|
-
{ constraintId: 'test', type: 'high_failure_rate' },
|
|
264
|
-
]),
|
|
265
|
-
};
|
|
266
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
186
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
187
|
+
{ constraintId: 'test', result: 'fail' },
|
|
188
|
+
]));
|
|
267
189
|
|
|
268
|
-
await status({}, io);
|
|
190
|
+
await status({ anomalies: true }, io);
|
|
269
191
|
expect(io.outText()).toContain('harness status --detail');
|
|
270
192
|
});
|
|
271
193
|
});
|
|
272
194
|
|
|
273
|
-
describe('
|
|
195
|
+
describe('详细模式扩展', () => {
|
|
274
196
|
it('应该显示 Guidelines 详细统计', async () => {
|
|
275
|
-
mockFs.readFileSync.mockReturnValue(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{ constraintId: 'test_guide', level: 'guideline', passRate: 0.6, failRate: 0.4, totalChecks: 10 },
|
|
280
|
-
]),
|
|
281
|
-
detectAnomalies: jest.fn().mockReturnValue([]),
|
|
282
|
-
};
|
|
283
|
-
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
197
|
+
mockFs.readFileSync.mockReturnValue(traceFile([
|
|
198
|
+
...repeats('test_guide', 3, 'pass'),
|
|
199
|
+
...repeats('test_guide', 2, 'fail'),
|
|
200
|
+
].map(t => ({ ...t, level: 'guideline' as const }))));
|
|
284
201
|
|
|
285
202
|
await status({ detail: true }, io);
|
|
286
|
-
expect(io.outText()).toContain('
|
|
203
|
+
expect(io.outText()).toContain('⚠️ test_guide');
|
|
204
|
+
expect(io.outText()).toContain('检查: 5 | 通过: 60% | 失败: 40%');
|
|
287
205
|
});
|
|
288
206
|
});
|
|
289
207
|
});
|
|
@@ -21,9 +21,35 @@ import { GOVERNANCE_HEADING } from '../../core/constraints/injection-writer';
|
|
|
21
21
|
import { getTraceCollector } from '../../monitoring/traces';
|
|
22
22
|
import { countJsonlLines } from '../../utils/jsonl';
|
|
23
23
|
import { DEFAULT_TRACE_FILE } from '../../types/trace';
|
|
24
|
-
import type { ConstraintTrigger } from '../../types/constraint';
|
|
24
|
+
import type { ConstraintResult, ConstraintTrigger } from '../../types/constraint';
|
|
25
25
|
import { log, processIO, type CommandIO, type CommandResult } from '../command-contract';
|
|
26
26
|
|
|
27
|
+
/** 证据行着色(与调用处所属结论块一致) */
|
|
28
|
+
const EVIDENCE_PAINT = {
|
|
29
|
+
red: chalk.red,
|
|
30
|
+
yellow: chalk.yellow,
|
|
31
|
+
gray: chalk.gray,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 打印 checker 的判定证据行(harness#119)
|
|
36
|
+
*
|
|
37
|
+
* 文案由 checker 自行措辞(每行自描述),CLI 只负责缩进与着色——
|
|
38
|
+
* 不在这里解释内容,否则证据形状与措辞会两头漂移。
|
|
39
|
+
* 传 id 时首行挂上约束 id(提示块里没有父级结论行可依附)。
|
|
40
|
+
*/
|
|
41
|
+
function logEvidence(
|
|
42
|
+
io: CommandIO,
|
|
43
|
+
result: ConstraintResult,
|
|
44
|
+
color: keyof typeof EVIDENCE_PAINT,
|
|
45
|
+
id?: string
|
|
46
|
+
): void {
|
|
47
|
+
const paint = EVIDENCE_PAINT[color];
|
|
48
|
+
(result.evidence ?? []).forEach((line, index) => {
|
|
49
|
+
log(io, paint(index === 0 && id ? ` - ${id}: ${line}` : ` ${line}`));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
export interface CheckOptions {
|
|
28
54
|
/** 预设名称 */
|
|
29
55
|
preset: string;
|
|
@@ -110,6 +136,7 @@ export async function check(
|
|
|
110
136
|
if (r.constraint) {
|
|
111
137
|
log(io, chalk.red(` - ${r.constraint.id}: ${r.constraint.message}`));
|
|
112
138
|
log(io, chalk.red(` ${r.constraint.rule}`));
|
|
139
|
+
logEvidence(io, r, 'red');
|
|
113
140
|
}
|
|
114
141
|
});
|
|
115
142
|
log(io);
|
|
@@ -126,6 +153,7 @@ export async function check(
|
|
|
126
153
|
result.guidelines.filter(r => !r.satisfied).forEach(r => {
|
|
127
154
|
if (r.constraint) {
|
|
128
155
|
log(io, chalk.yellow(` - ${r.constraint.id}: ${r.constraint.message}`));
|
|
156
|
+
logEvidence(io, r, 'yellow');
|
|
129
157
|
}
|
|
130
158
|
});
|
|
131
159
|
} else if (result.guidelines.length > 0) {
|
|
@@ -134,6 +162,16 @@ export async function check(
|
|
|
134
162
|
log(io, chalk.green(`✅ 指导原则: ${passedGuidelines}/${evaluatedGuidelines.length} 通过`));
|
|
135
163
|
}
|
|
136
164
|
|
|
165
|
+
// 提示:通过但带证据(harness#119)——与本次变更无因果的仓库级漂移在此露出,
|
|
166
|
+
// 不判违规、不改 exit code,只保证「看得见且能自己修」
|
|
167
|
+
const hints = [...result.ironLaws, ...result.guidelines].filter(
|
|
168
|
+
r => !r.skipped && r.satisfied && (r.evidence?.length ?? 0) > 0
|
|
169
|
+
);
|
|
170
|
+
if (hints.length > 0) {
|
|
171
|
+
log(io, chalk.gray(`💡 提示: ${hints.length} 条(不判违规,供参考)`));
|
|
172
|
+
hints.forEach(r => logEvidence(io, r, 'gray', r.id));
|
|
173
|
+
}
|
|
174
|
+
|
|
137
175
|
// Skipped:约定未采用 / 证据未接线,未评估(不计通过/失败)
|
|
138
176
|
if (skippedResults.length > 0) {
|
|
139
177
|
log(io, chalk.gray(`⏭️ 跳过评估: ${skippedResults.length} 条(约定未采用或证据未接线,不计通过/失败)`));
|