@dommaker/harness 1.5.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 (208) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +0 -2
  3. package/dist/cli/commands/definitions.d.ts.map +1 -1
  4. package/dist/cli/commands/definitions.js +1 -32
  5. package/dist/cli/commands/definitions.js.map +1 -1
  6. package/dist/cli/commands/passes-gate.d.ts +0 -2
  7. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  8. package/dist/cli/commands/passes-gate.js +0 -1
  9. package/dist/cli/commands/passes-gate.js.map +1 -1
  10. package/dist/cli/commands/performance.d.ts +0 -4
  11. package/dist/cli/commands/performance.d.ts.map +1 -1
  12. package/dist/cli/commands/performance.js +10 -20
  13. package/dist/cli/commands/performance.js.map +1 -1
  14. package/dist/cli/commands/report.d.ts +1 -1
  15. package/dist/cli/commands/report.d.ts.map +1 -1
  16. package/dist/cli/commands/report.js +2 -55
  17. package/dist/cli/commands/report.js.map +1 -1
  18. package/dist/cli/commands/status.d.ts.map +1 -1
  19. package/dist/cli/commands/status.js +4 -8
  20. package/dist/cli/commands/status.js.map +1 -1
  21. package/dist/context/index.d.ts +2 -5
  22. package/dist/context/index.d.ts.map +1 -1
  23. package/dist/context/index.js +2 -8
  24. package/dist/context/index.js.map +1 -1
  25. package/dist/context/token-budget.d.ts +0 -29
  26. package/dist/context/token-budget.d.ts.map +1 -1
  27. package/dist/context/token-budget.js +1 -65
  28. package/dist/context/token-budget.js.map +1 -1
  29. package/dist/context/types.d.ts +0 -1
  30. package/dist/context/types.d.ts.map +1 -1
  31. package/dist/context/types.js +0 -9
  32. package/dist/context/types.js.map +1 -1
  33. package/dist/core/constraints/checker.d.ts +14 -0
  34. package/dist/core/constraints/checker.d.ts.map +1 -1
  35. package/dist/core/constraints/checker.js +22 -2
  36. package/dist/core/constraints/checker.js.map +1 -1
  37. package/dist/core/constraints/git-evidence.d.ts +6 -3
  38. package/dist/core/constraints/git-evidence.d.ts.map +1 -1
  39. package/dist/core/constraints/git-evidence.js +6 -3
  40. package/dist/core/constraints/git-evidence.js.map +1 -1
  41. package/dist/core/index.d.ts +1 -2
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +1 -9
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/spec/validator.d.ts +2 -0
  46. package/dist/core/spec/validator.d.ts.map +1 -1
  47. package/dist/core/spec/validator.js +9 -11
  48. package/dist/core/spec/validator.js.map +1 -1
  49. package/dist/core/validators/index.d.ts +1 -1
  50. package/dist/core/validators/index.d.ts.map +1 -1
  51. package/dist/core/validators/passes-gate.d.ts +1 -15
  52. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  53. package/dist/core/validators/passes-gate.js +0 -125
  54. package/dist/core/validators/passes-gate.js.map +1 -1
  55. package/dist/failure/index.d.ts +0 -1
  56. package/dist/failure/index.d.ts.map +1 -1
  57. package/dist/failure/index.js +1 -7
  58. package/dist/failure/index.js.map +1 -1
  59. package/dist/gates/definitions.d.ts.map +1 -1
  60. package/dist/gates/definitions.js +0 -4
  61. package/dist/gates/definitions.js.map +1 -1
  62. package/dist/gates/performance.d.ts +7 -22
  63. package/dist/gates/performance.d.ts.map +1 -1
  64. package/dist/gates/performance.js +7 -89
  65. package/dist/gates/performance.js.map +1 -1
  66. package/dist/gates/types.d.ts +2 -8
  67. package/dist/gates/types.d.ts.map +1 -1
  68. package/dist/index.d.ts +6 -8
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -22
  71. package/dist/index.js.map +1 -1
  72. package/dist/knowledge/index.d.ts +0 -1
  73. package/dist/knowledge/index.d.ts.map +1 -1
  74. package/dist/knowledge/index.js +1 -3
  75. package/dist/knowledge/index.js.map +1 -1
  76. package/dist/monitoring/trace-analyzer.d.ts +25 -10
  77. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  78. package/dist/monitoring/trace-analyzer.js +147 -116
  79. package/dist/monitoring/trace-analyzer.js.map +1 -1
  80. package/dist/types/passes-gate.d.ts +0 -19
  81. package/dist/types/passes-gate.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/__tests__/passes-gate-check.test.ts +20 -4
  84. package/src/__tests__/passes-gate.test.ts +5 -99
  85. package/src/__tests__/performance-gate.test.ts +3 -11
  86. package/src/__tests__/public-exports.test.ts +46 -16
  87. package/src/__tests__/trace-analyzer.test.ts +52 -2
  88. package/src/cli/commands/CONTEXT.md +3 -2
  89. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +70 -0
  90. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  91. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  92. package/src/cli/commands/__tests__/registry.test.ts +46 -3
  93. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  94. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  95. package/src/cli/commands/__tests__/report.test.ts +0 -20
  96. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  97. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  98. package/src/cli/commands/__tests__/status.test.ts +66 -148
  99. package/src/cli/commands/definitions.ts +1 -32
  100. package/src/cli/commands/passes-gate.ts +0 -3
  101. package/src/cli/commands/performance.ts +13 -27
  102. package/src/cli/commands/report.ts +3 -61
  103. package/src/cli/commands/status.ts +5 -10
  104. package/src/context/CONTEXT.md +2 -3
  105. package/src/context/__tests__/token-budget.test.ts +1 -81
  106. package/src/context/index.ts +2 -7
  107. package/src/context/token-budget.ts +0 -73
  108. package/src/context/types.ts +0 -9
  109. package/src/core/CONTEXT.md +5 -6
  110. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  111. package/src/core/constraints/checker.ts +33 -2
  112. package/src/core/constraints/git-evidence.ts +6 -3
  113. package/src/core/index.ts +0 -11
  114. package/src/core/spec/__tests__/validator.test.ts +24 -0
  115. package/src/core/spec/validator.ts +9 -10
  116. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  117. package/src/core/validators/index.ts +0 -1
  118. package/src/core/validators/passes-gate.ts +1 -149
  119. package/src/failure/CONTEXT.md +8 -6
  120. package/src/failure/index.ts +0 -10
  121. package/src/gates/CONTEXT.md +3 -2
  122. package/src/gates/__tests__/performance.test.ts +6 -74
  123. package/src/gates/definitions.ts +0 -4
  124. package/src/gates/performance.ts +9 -111
  125. package/src/gates/types.ts +2 -8
  126. package/src/index.ts +1 -36
  127. package/src/knowledge/CONTEXT.md +1 -2
  128. package/src/knowledge/index.ts +0 -1
  129. package/src/monitoring/CONTEXT.md +2 -2
  130. package/src/monitoring/trace-analyzer.ts +160 -126
  131. package/src/types/passes-gate.ts +0 -26
  132. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  133. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  134. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  135. package/dist/cli/commands/analyze-sessions.js +0 -290
  136. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  137. package/dist/cli/commands/update-user-model.d.ts +0 -30
  138. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  139. package/dist/cli/commands/update-user-model.js +0 -406
  140. package/dist/cli/commands/update-user-model.js.map +0 -1
  141. package/dist/cli/session-mining/corrections.d.ts +0 -17
  142. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  143. package/dist/cli/session-mining/corrections.js +0 -58
  144. package/dist/cli/session-mining/corrections.js.map +0 -1
  145. package/dist/cli/session-mining/index.d.ts +0 -13
  146. package/dist/cli/session-mining/index.d.ts.map +0 -1
  147. package/dist/cli/session-mining/index.js +0 -29
  148. package/dist/cli/session-mining/index.js.map +0 -1
  149. package/dist/cli/session-mining/text.d.ts +0 -31
  150. package/dist/cli/session-mining/text.d.ts.map +0 -1
  151. package/dist/cli/session-mining/text.js +0 -126
  152. package/dist/cli/session-mining/text.js.map +0 -1
  153. package/dist/cli/session-mining/transcript.d.ts +0 -34
  154. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  155. package/dist/cli/session-mining/transcript.js +0 -130
  156. package/dist/cli/session-mining/transcript.js.map +0 -1
  157. package/dist/context/compaction.d.ts +0 -57
  158. package/dist/context/compaction.d.ts.map +0 -1
  159. package/dist/context/compaction.js +0 -165
  160. package/dist/context/compaction.js.map +0 -1
  161. package/dist/core/session/clean-state.d.ts +0 -17
  162. package/dist/core/session/clean-state.d.ts.map +0 -1
  163. package/dist/core/session/clean-state.js +0 -192
  164. package/dist/core/session/clean-state.js.map +0 -1
  165. package/dist/core/session/index.d.ts +0 -6
  166. package/dist/core/session/index.d.ts.map +0 -1
  167. package/dist/core/session/index.js +0 -15
  168. package/dist/core/session/index.js.map +0 -1
  169. package/dist/core/session/startup.d.ts +0 -47
  170. package/dist/core/session/startup.d.ts.map +0 -1
  171. package/dist/core/session/startup.js +0 -263
  172. package/dist/core/session/startup.js.map +0 -1
  173. package/dist/failure/constraint-handler.d.ts +0 -83
  174. package/dist/failure/constraint-handler.d.ts.map +0 -1
  175. package/dist/failure/constraint-handler.js +0 -138
  176. package/dist/failure/constraint-handler.js.map +0 -1
  177. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  178. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  179. package/dist/knowledge/lifecycle-hooks.js +0 -143
  180. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  181. package/dist/types/session.d.ts +0 -113
  182. package/dist/types/session.d.ts.map +0 -1
  183. package/dist/types/session.js +0 -6
  184. package/dist/types/session.js.map +0 -1
  185. package/src/__tests__/clean-state-extra.test.ts +0 -301
  186. package/src/__tests__/clean-state.test.ts +0 -169
  187. package/src/__tests__/constraint-handler.test.ts +0 -132
  188. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  189. package/src/__tests__/session-startup.test.ts +0 -334
  190. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  191. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -268
  192. package/src/cli/commands/analyze-sessions.ts +0 -335
  193. package/src/cli/commands/update-user-model.ts +0 -476
  194. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  195. package/src/cli/session-mining/corrections.ts +0 -56
  196. package/src/cli/session-mining/index.ts +0 -13
  197. package/src/cli/session-mining/text.ts +0 -117
  198. package/src/cli/session-mining/transcript.ts +0 -125
  199. package/src/context/__tests__/compaction.test.ts +0 -169
  200. package/src/context/compaction.ts +0 -201
  201. package/src/core/session/__tests__/startup.test.ts +0 -281
  202. package/src/core/session/clean-state.ts +0 -178
  203. package/src/core/session/index.ts +0 -15
  204. package/src/core/session/startup.ts +0 -253
  205. package/src/failure/constraint-handler.ts +0 -171
  206. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  207. package/src/knowledge/lifecycle-hooks.ts +0 -202
  208. package/src/types/session.ts +0 -132
@@ -52,18 +52,6 @@ describe('report command - 补充覆盖', () => {
52
52
  });
53
53
  });
54
54
 
55
- describe('HTML 报告完整内容', () => {
56
- it('HTML 报告应该包含所有区块', async () => {
57
- await report({ format: 'html' }, io);
58
- const output = io.outText();
59
-
60
- expect(output).toContain('<!DOCTYPE html>');
61
- expect(output).toContain('<title>Harness 检查报告</title>');
62
- expect(output).toContain('约束检查');
63
- expect(output).toContain('Iron Laws');
64
- });
65
- });
66
-
67
55
  describe('文件输出', () => {
68
56
  it('Markdown 输出到文件', async () => {
69
57
  mockFs.writeFile.mockResolvedValue(undefined);
@@ -71,13 +59,6 @@ describe('report command - 补充覆盖', () => {
71
59
  await report({ format: 'markdown', output: 'report.md' }, io);
72
60
  expect(mockFs.writeFile).toHaveBeenCalledWith('report.md', expect.stringContaining('# Harness'), 'utf-8');
73
61
  });
74
-
75
- it('HTML 输出到文件', async () => {
76
- mockFs.writeFile.mockResolvedValue(undefined);
77
-
78
- await report({ format: 'html', output: 'report.html' }, io);
79
- expect(mockFs.writeFile).toHaveBeenCalledWith('report.html', expect.stringContaining('<!DOCTYPE html>'), 'utf-8');
80
- });
81
62
  });
82
63
 
83
64
  describe('JSON 报告内容', () => {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * report 假绿回归(架构评审候选1)
3
+ *
4
+ * 历史 bug:report 经 executeWithCollect 调 checkConstraints,checker 在首个
5
+ * 铁律违规即 throw,handler 的 catch 丢弃 error.result 并合成空结果 ——
6
+ * 铁律违规被报成 violations: [] / passed = 全量(假绿)。
7
+ * 修复后 report 直调 checker.collectConstraints(不抛、全量收集),违规如实上报。
8
+ *
9
+ * fixture:CAPABILITIES.md 登记幽灵文件 → docs_freshness(file_modification
10
+ * 触发域内唯一铁律)必违规。
11
+ */
12
+
13
+ import { describe, it, expect } from '@jest/globals';
14
+ import { report } from '../report';
15
+ import { captureIO } from '../../command-contract';
16
+ import { createProjectFixture } from '../../../test-setup/project-fixture';
17
+
18
+ // 与 three-state-consistency.test.ts 相同的表格正本形状
19
+ const GHOST_CAPABILITIES =
20
+ '# Capabilities\n\n' +
21
+ '| 模块 | 文件 | 说明 |\n' +
22
+ '|------|------|------|\n' +
23
+ '| 遗魂 | src/gone.ts | 已删 |\n';
24
+
25
+ function parseJsonReport(output: string) {
26
+ const start = output.indexOf('{');
27
+ const end = output.lastIndexOf('}');
28
+ return JSON.parse(output.slice(start, end + 1));
29
+ }
30
+
31
+ describe('report 铁律违规如实上报(假绿回归)', () => {
32
+ it('docs_freshness 违规 → failed ≥ 1、violations 点名、passed < total', async () => {
33
+ const root = createProjectFixture({
34
+ name: 'report-false-green',
35
+ files: { 'CAPABILITIES.md': GHOST_CAPABILITIES },
36
+ });
37
+ const io = captureIO();
38
+
39
+ await report({ format: 'json', projectPath: root }, io);
40
+
41
+ const data = parseJsonReport(io.outText());
42
+ expect(data.constraints.failed).toBeGreaterThanOrEqual(1);
43
+ expect(data.constraints.violations.map((v: { id: string }) => v.id)).toContain('docs_freshness');
44
+ expect(data.constraints.passed).toBeLessThan(data.constraints.total);
45
+ });
46
+ });
@@ -44,11 +44,6 @@ describe('report command', () => {
44
44
  expect(io.outText()).toContain('Harness 检查报告');
45
45
  });
46
46
 
47
- it('应该生成 HTML 格式报告', async () => {
48
- await report({ format: 'html' }, io);
49
- expect(io.outText()).toContain('<!DOCTYPE html>');
50
- });
51
-
52
47
  it('应该保存报告到文件', async () => {
53
48
  mockFs.writeFile.mockResolvedValue(undefined);
54
49
 
@@ -70,15 +65,6 @@ describe('report command', () => {
70
65
  expect(output).toContain('报告由 @dommaker/harness 生成');
71
66
  });
72
67
 
73
- it('HTML 报告应包含完整结构', async () => {
74
- await report({ format: 'html' }, io);
75
- const output = io.outText();
76
- expect(output).toContain('<html lang="zh-CN">');
77
- expect(output).toContain('</html>');
78
- expect(output).toContain('Harness 检查报告');
79
- expect(output).toContain('<table>');
80
- });
81
-
82
68
  it('JSON 报告应包含约束统计', async () => {
83
69
  await report({ format: 'json' }, io);
84
70
  const output = io.outText();
@@ -99,11 +85,5 @@ describe('report command', () => {
99
85
  await report({ format: 'markdown', output: 'report.md' }, io);
100
86
  expect(mockFs.writeFile).toHaveBeenCalledWith('report.md', expect.stringContaining('# Harness'), 'utf-8');
101
87
  });
102
-
103
- it('应该支持输出到文件 (html)', async () => {
104
- mockFs.writeFile.mockResolvedValue(undefined);
105
- await report({ format: 'html', output: 'report.html' }, io);
106
- expect(mockFs.writeFile).toHaveBeenCalledWith('report.html', expect.stringContaining('<!DOCTYPE html>'), 'utf-8');
107
- });
108
88
  });
109
89
  });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * status 的分析接线(ADR-0020)
3
+ *
4
+ * `status` 要的是「对已读出的 traces 做纯判定」,此前却为此构造
5
+ * `new TraceCollector({traceFile})`(构造函数带 mkdir 副作用)+ `new TraceAnalyzer(c)`,
6
+ * 测试端再叠一层 TraceAnalyzer mock 才能断言。ADR-0020 把判定下放为模块级纯函数后,
7
+ * 本命令直调函数,构造仪式退出这条路径。
8
+ *
9
+ * 用真实临时工程根跑一遍(不 mock fs),顺带钉住「换成直调后统计与异常输出口径不变」。
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { createProjectFixture } from '../../../test-setup/project-fixture';
15
+ import { captureIO, type CapturingIO } from '../../command-contract';
16
+ import { status } from '../status';
17
+
18
+ const STATUS_SOURCE = fs.readFileSync(path.join(__dirname, '..', 'status.ts'), 'utf-8');
19
+
20
+ /** 1 pass + 1 fail:failRate 0.5、passRate 0.5 → 无异常;2 fail:passRate 0 → low_pass_rate */
21
+ const PASS = { constraintId: 'alpha', level: 'iron_law' as const, result: 'pass' as const };
22
+ const FAIL = { constraintId: 'beta', level: 'guideline' as const, result: 'fail' as const };
23
+
24
+ let io: CapturingIO;
25
+ beforeEach(() => {
26
+ io = captureIO();
27
+ });
28
+
29
+ describe('status 分析接线(ADR-0020)', () => {
30
+ it('不再构造 TraceCollector / TraceAnalyzer', () => {
31
+ expect(STATUS_SOURCE).not.toMatch(/new\s+TraceCollector\s*\(/);
32
+ expect(STATUS_SOURCE).not.toMatch(/new\s+TraceAnalyzer\s*\(/);
33
+ expect(STATUS_SOURCE).not.toMatch(/from\s+'[^']*monitoring\/traces'/);
34
+ });
35
+
36
+ it('直调 trace-analyzer 的模块级纯函数取统计与异常', () => {
37
+ expect(STATUS_SOURCE).toMatch(/summarizeTraces\s*\(/);
38
+ expect(STATUS_SOURCE).toMatch(/detectTraceAnomalies\s*\(/);
39
+ });
40
+
41
+ it('真实工程根跑通:按纯函数口径出统计,全过数据零异常', async () => {
42
+ const root = createProjectFixture({
43
+ name: 'status-pure-analysis',
44
+ config: 'project:\n name: status-pure-analysis\n',
45
+ traces: [PASS, { ...PASS, timestamp: 2 }],
46
+ });
47
+
48
+ await status({ projectPath: root, detail: true }, io);
49
+
50
+ expect(io.outText()).toContain('🔴 Iron Laws:');
51
+ expect(io.outText()).toContain('alpha');
52
+ expect(io.outText()).toContain('检查: 2 | 通过: 100% | 失败: 0%');
53
+ expect(io.outText()).not.toContain('个异常');
54
+ expect(io.outText()).toContain('继续积累数据');
55
+ });
56
+
57
+ it('真实工程根跑通:低通过率经纯函数判为异常并列出', async () => {
58
+ const root = createProjectFixture({
59
+ name: 'status-pure-analysis',
60
+ traces: [FAIL, { ...FAIL, timestamp: 2 }],
61
+ });
62
+
63
+ await status({ projectPath: root, anomalies: true }, io);
64
+
65
+ expect(io.outText()).toContain('发现 1 个异常');
66
+ expect(io.outText()).toContain('类型: low_pass_rate');
67
+ });
68
+ });
@@ -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
+ });