@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.
Files changed (258) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +0 -2
  3. package/dist/cli/commands/check.d.ts.map +1 -1
  4. package/dist/cli/commands/check.js +28 -0
  5. package/dist/cli/commands/check.js.map +1 -1
  6. package/dist/cli/commands/definitions.d.ts.map +1 -1
  7. package/dist/cli/commands/definitions.js +1 -39
  8. package/dist/cli/commands/definitions.js.map +1 -1
  9. package/dist/cli/commands/knowledge.d.ts +0 -19
  10. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  11. package/dist/cli/commands/knowledge.js +0 -105
  12. package/dist/cli/commands/knowledge.js.map +1 -1
  13. package/dist/cli/commands/passes-gate.d.ts +0 -2
  14. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  15. package/dist/cli/commands/passes-gate.js +0 -1
  16. package/dist/cli/commands/passes-gate.js.map +1 -1
  17. package/dist/cli/commands/performance.d.ts +0 -4
  18. package/dist/cli/commands/performance.d.ts.map +1 -1
  19. package/dist/cli/commands/performance.js +10 -20
  20. package/dist/cli/commands/performance.js.map +1 -1
  21. package/dist/cli/commands/report.d.ts +1 -1
  22. package/dist/cli/commands/report.d.ts.map +1 -1
  23. package/dist/cli/commands/report.js +2 -55
  24. package/dist/cli/commands/report.js.map +1 -1
  25. package/dist/cli/commands/status.d.ts.map +1 -1
  26. package/dist/cli/commands/status.js +4 -8
  27. package/dist/cli/commands/status.js.map +1 -1
  28. package/dist/context/index.d.ts +2 -5
  29. package/dist/context/index.d.ts.map +1 -1
  30. package/dist/context/index.js +2 -8
  31. package/dist/context/index.js.map +1 -1
  32. package/dist/context/token-budget.d.ts +0 -29
  33. package/dist/context/token-budget.d.ts.map +1 -1
  34. package/dist/context/token-budget.js +1 -65
  35. package/dist/context/token-budget.js.map +1 -1
  36. package/dist/context/types.d.ts +0 -1
  37. package/dist/context/types.d.ts.map +1 -1
  38. package/dist/context/types.js +0 -9
  39. package/dist/context/types.js.map +1 -1
  40. package/dist/core/constraints/checker.d.ts +14 -0
  41. package/dist/core/constraints/checker.d.ts.map +1 -1
  42. package/dist/core/constraints/checker.js +27 -5
  43. package/dist/core/constraints/checker.js.map +1 -1
  44. package/dist/core/constraints/checkers/capability-sync.d.ts +17 -6
  45. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  46. package/dist/core/constraints/checkers/capability-sync.js +48 -19
  47. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  48. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  49. package/dist/core/constraints/checkers/docs-freshness.js +15 -6
  50. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  51. package/dist/core/constraints/checkers/index.d.ts +2 -2
  52. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  53. package/dist/core/constraints/checkers/index.js +2 -1
  54. package/dist/core/constraints/checkers/index.js.map +1 -1
  55. package/dist/core/constraints/checkers/types.d.ts +41 -2
  56. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  57. package/dist/core/constraints/checkers/types.js +33 -0
  58. package/dist/core/constraints/checkers/types.js.map +1 -1
  59. package/dist/core/constraints/git-evidence.d.ts +6 -3
  60. package/dist/core/constraints/git-evidence.d.ts.map +1 -1
  61. package/dist/core/constraints/git-evidence.js +6 -3
  62. package/dist/core/constraints/git-evidence.js.map +1 -1
  63. package/dist/core/index.d.ts +1 -2
  64. package/dist/core/index.d.ts.map +1 -1
  65. package/dist/core/index.js +1 -9
  66. package/dist/core/index.js.map +1 -1
  67. package/dist/core/spec/validator.d.ts +2 -0
  68. package/dist/core/spec/validator.d.ts.map +1 -1
  69. package/dist/core/spec/validator.js +9 -11
  70. package/dist/core/spec/validator.js.map +1 -1
  71. package/dist/core/validators/index.d.ts +1 -1
  72. package/dist/core/validators/index.d.ts.map +1 -1
  73. package/dist/core/validators/passes-gate.d.ts +1 -15
  74. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  75. package/dist/core/validators/passes-gate.js +0 -125
  76. package/dist/core/validators/passes-gate.js.map +1 -1
  77. package/dist/failure/index.d.ts +0 -1
  78. package/dist/failure/index.d.ts.map +1 -1
  79. package/dist/failure/index.js +1 -7
  80. package/dist/failure/index.js.map +1 -1
  81. package/dist/gates/checker-gate.d.ts +2 -1
  82. package/dist/gates/checker-gate.d.ts.map +1 -1
  83. package/dist/gates/checker-gate.js +10 -7
  84. package/dist/gates/checker-gate.js.map +1 -1
  85. package/dist/gates/definitions.d.ts.map +1 -1
  86. package/dist/gates/definitions.js +0 -4
  87. package/dist/gates/definitions.js.map +1 -1
  88. package/dist/gates/performance.d.ts +7 -22
  89. package/dist/gates/performance.d.ts.map +1 -1
  90. package/dist/gates/performance.js +7 -89
  91. package/dist/gates/performance.js.map +1 -1
  92. package/dist/gates/types.d.ts +2 -8
  93. package/dist/gates/types.d.ts.map +1 -1
  94. package/dist/index.d.ts +6 -8
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +4 -22
  97. package/dist/index.js.map +1 -1
  98. package/dist/knowledge/index.d.ts +0 -1
  99. package/dist/knowledge/index.d.ts.map +1 -1
  100. package/dist/knowledge/index.js +1 -3
  101. package/dist/knowledge/index.js.map +1 -1
  102. package/dist/monitoring/trace-analyzer.d.ts +25 -10
  103. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  104. package/dist/monitoring/trace-analyzer.js +147 -116
  105. package/dist/monitoring/trace-analyzer.js.map +1 -1
  106. package/dist/types/constraint.d.ts +8 -0
  107. package/dist/types/constraint.d.ts.map +1 -1
  108. package/dist/types/constraint.js +6 -1
  109. package/dist/types/constraint.js.map +1 -1
  110. package/dist/types/passes-gate.d.ts +0 -19
  111. package/dist/types/passes-gate.d.ts.map +1 -1
  112. package/dist/types/trace.d.ts +9 -0
  113. package/dist/types/trace.d.ts.map +1 -1
  114. package/package.json +1 -1
  115. package/src/CONTEXT.md +1 -0
  116. package/src/__tests__/passes-gate-check.test.ts +20 -4
  117. package/src/__tests__/passes-gate.test.ts +18 -119
  118. package/src/__tests__/performance-gate.test.ts +3 -11
  119. package/src/__tests__/public-exports.test.ts +46 -16
  120. package/src/__tests__/trace-analyzer.test.ts +52 -2
  121. package/src/cli/commands/CONTEXT.md +5 -4
  122. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +6 -0
  123. package/src/cli/commands/__tests__/check.test.ts +74 -0
  124. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +70 -0
  125. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  126. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  127. package/src/cli/commands/__tests__/project-path-convention.test.ts +85 -23
  128. package/src/cli/commands/__tests__/registry.test.ts +52 -3
  129. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  130. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  131. package/src/cli/commands/__tests__/report.test.ts +0 -20
  132. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  133. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  134. package/src/cli/commands/__tests__/status.test.ts +66 -148
  135. package/src/cli/commands/check.ts +39 -1
  136. package/src/cli/commands/definitions.ts +1 -39
  137. package/src/cli/commands/knowledge.ts +0 -119
  138. package/src/cli/commands/passes-gate.ts +0 -3
  139. package/src/cli/commands/performance.ts +13 -27
  140. package/src/cli/commands/report.ts +3 -61
  141. package/src/cli/commands/status.ts +5 -10
  142. package/src/context/CONTEXT.md +2 -3
  143. package/src/context/__tests__/token-budget.test.ts +1 -81
  144. package/src/context/index.ts +2 -7
  145. package/src/context/token-budget.ts +0 -73
  146. package/src/context/types.ts +0 -9
  147. package/src/core/CONTEXT.md +6 -6
  148. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  149. package/src/core/constraints/checker.ts +41 -6
  150. package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +90 -31
  151. package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +176 -0
  152. package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +18 -9
  153. package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +12 -4
  154. package/src/core/constraints/checkers/capability-sync.ts +56 -18
  155. package/src/core/constraints/checkers/docs-freshness.ts +18 -5
  156. package/src/core/constraints/checkers/index.ts +9 -2
  157. package/src/core/constraints/checkers/types.ts +58 -2
  158. package/src/core/constraints/git-evidence.ts +6 -3
  159. package/src/core/index.ts +0 -11
  160. package/src/core/spec/__tests__/validator.test.ts +24 -0
  161. package/src/core/spec/validator.ts +9 -10
  162. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  163. package/src/core/validators/index.ts +0 -1
  164. package/src/core/validators/passes-gate.ts +1 -149
  165. package/src/failure/CONTEXT.md +8 -6
  166. package/src/failure/index.ts +0 -10
  167. package/src/gates/CONTEXT.md +4 -3
  168. package/src/gates/__tests__/checker-gate.test.ts +24 -0
  169. package/src/gates/__tests__/performance.test.ts +6 -74
  170. package/src/gates/checker-gate.ts +11 -15
  171. package/src/gates/definitions.ts +0 -4
  172. package/src/gates/performance.ts +9 -111
  173. package/src/gates/types.ts +2 -8
  174. package/src/index.ts +1 -36
  175. package/src/knowledge/CONTEXT.md +1 -2
  176. package/src/knowledge/index.ts +0 -1
  177. package/src/monitoring/CONTEXT.md +2 -2
  178. package/src/monitoring/trace-analyzer.ts +160 -126
  179. package/src/types/constraint.ts +16 -2
  180. package/src/types/passes-gate.ts +0 -26
  181. package/src/types/trace.ts +10 -0
  182. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  183. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  184. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  185. package/dist/cli/commands/analyze-sessions.js +0 -290
  186. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  187. package/dist/cli/commands/update-user-model.d.ts +0 -25
  188. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  189. package/dist/cli/commands/update-user-model.js +0 -397
  190. package/dist/cli/commands/update-user-model.js.map +0 -1
  191. package/dist/cli/session-mining/corrections.d.ts +0 -17
  192. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  193. package/dist/cli/session-mining/corrections.js +0 -58
  194. package/dist/cli/session-mining/corrections.js.map +0 -1
  195. package/dist/cli/session-mining/index.d.ts +0 -13
  196. package/dist/cli/session-mining/index.d.ts.map +0 -1
  197. package/dist/cli/session-mining/index.js +0 -29
  198. package/dist/cli/session-mining/index.js.map +0 -1
  199. package/dist/cli/session-mining/text.d.ts +0 -31
  200. package/dist/cli/session-mining/text.d.ts.map +0 -1
  201. package/dist/cli/session-mining/text.js +0 -126
  202. package/dist/cli/session-mining/text.js.map +0 -1
  203. package/dist/cli/session-mining/transcript.d.ts +0 -34
  204. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  205. package/dist/cli/session-mining/transcript.js +0 -130
  206. package/dist/cli/session-mining/transcript.js.map +0 -1
  207. package/dist/context/compaction.d.ts +0 -57
  208. package/dist/context/compaction.d.ts.map +0 -1
  209. package/dist/context/compaction.js +0 -165
  210. package/dist/context/compaction.js.map +0 -1
  211. package/dist/core/session/clean-state.d.ts +0 -17
  212. package/dist/core/session/clean-state.d.ts.map +0 -1
  213. package/dist/core/session/clean-state.js +0 -192
  214. package/dist/core/session/clean-state.js.map +0 -1
  215. package/dist/core/session/index.d.ts +0 -6
  216. package/dist/core/session/index.d.ts.map +0 -1
  217. package/dist/core/session/index.js +0 -15
  218. package/dist/core/session/index.js.map +0 -1
  219. package/dist/core/session/startup.d.ts +0 -47
  220. package/dist/core/session/startup.d.ts.map +0 -1
  221. package/dist/core/session/startup.js +0 -263
  222. package/dist/core/session/startup.js.map +0 -1
  223. package/dist/failure/constraint-handler.d.ts +0 -83
  224. package/dist/failure/constraint-handler.d.ts.map +0 -1
  225. package/dist/failure/constraint-handler.js +0 -138
  226. package/dist/failure/constraint-handler.js.map +0 -1
  227. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  228. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  229. package/dist/knowledge/lifecycle-hooks.js +0 -143
  230. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  231. package/dist/types/session.d.ts +0 -113
  232. package/dist/types/session.d.ts.map +0 -1
  233. package/dist/types/session.js +0 -6
  234. package/dist/types/session.js.map +0 -1
  235. package/src/__tests__/clean-state-extra.test.ts +0 -301
  236. package/src/__tests__/clean-state.test.ts +0 -169
  237. package/src/__tests__/constraint-handler.test.ts +0 -132
  238. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  239. package/src/__tests__/session-startup.test.ts +0 -334
  240. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  241. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -234
  242. package/src/cli/commands/analyze-sessions.ts +0 -335
  243. package/src/cli/commands/update-user-model.ts +0 -461
  244. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  245. package/src/cli/session-mining/corrections.ts +0 -56
  246. package/src/cli/session-mining/index.ts +0 -13
  247. package/src/cli/session-mining/text.ts +0 -117
  248. package/src/cli/session-mining/transcript.ts +0 -125
  249. package/src/context/__tests__/compaction.test.ts +0 -169
  250. package/src/context/compaction.ts +0 -201
  251. package/src/core/session/__tests__/startup.test.ts +0 -281
  252. package/src/core/session/clean-state.ts +0 -178
  253. package/src/core/session/index.ts +0 -15
  254. package/src/core/session/startup.ts +0 -253
  255. package/src/failure/constraint-handler.ts +0 -171
  256. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  257. package/src/knowledge/lifecycle-hooks.ts +0 -202
  258. 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 { TraceAnalyzer } from '../../../monitoring/trace-analyzer';
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
- const MockTraceAnalyzer = TraceAnalyzer as jest.MockedClass<typeof TraceAnalyzer>;
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
- const mockTraces = [
64
- { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'fail' },
65
- ];
66
- mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
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(mockAnalyze.detectAnomalies).toHaveBeenCalled();
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(JSON.stringify({ constraintId: 'test', level: 'iron_law', result: 'pass' }));
99
-
100
- const mockAnalyze = {
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
- const output = io.outText();
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
- const mockTraces = [
116
- { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
117
- { constraintId: 'no_completion_without_verification', level: 'iron_law', result: 'pass' },
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
- const mockTraces = [
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()).not.toBe('');
108
+
109
+ expect(io.outText()).toContain('🟡 Guidelines:');
155
110
  });
156
111
  });
157
112
 
158
113
  describe('详细模式扩展', () => {
159
114
  it('详细模式应该显示所有级别详情', async () => {
160
- const mockTraces = [
161
- { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
162
- { constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
163
- ];
164
- mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
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('invalid json\n{"valid":"data"}\nalso invalid');
186
-
187
- const mockAnalyze = {
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
- expect(io.outText()).not.toBe('');
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 { TraceAnalyzer } from '../../../monitoring/trace-analyzer';
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
- const MockTraceAnalyzer = TraceAnalyzer as jest.MockedClass<typeof TraceAnalyzer>;
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
- const mockTraces = [
84
- { constraintId: 'test1', level: 'iron_law', result: 'pass' },
85
- { constraintId: 'test2', level: 'guideline', result: 'pass' },
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()).not.toBe('');
87
+ expect(io.outText()).toContain('记录数: 2 条');
88
+ expect(io.outText()).toContain('📈 约束统计:');
100
89
  });
101
90
 
102
91
  it('应该显示 Iron Laws 统计', async () => {
103
- const mockTraces = [
104
- { constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
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()).not.toBe('');
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
- const mockTraces = [
125
- { constraintId: 'test1', level: 'iron_law', result: 'fail' },
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
- const mockTraces = [
148
- { constraintId: 'test1', level: 'iron_law', result: 'pass' },
149
- ];
150
- mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
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()).not.toBe('');
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('{"constraintId":"test","level":"iron_law"}');
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('{"constraintId":"test"}');
184
-
185
- const mockAnalyze = {
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('{"constraintId":"test"}');
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('{"constraintId":"test"}');
214
-
215
- const mockAnalyze = {
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
- const traces = Array(100).fill('{"constraintId":"test"}').join('\n');
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('{"constraintId":"test"}');
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('{"constraintId":"test"}');
259
-
260
- const mockAnalyze = {
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('{"constraintId":"test"}');
276
-
277
- const mockAnalyze = {
278
- summarize: jest.fn().mockReturnValue([
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} 条(约定未采用或证据未接线,不计通过/失败)`));