@dommaker/harness 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +26 -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 +2 -127
  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 -32
  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 +68 -16
  87. package/src/__tests__/public-type-surface.test.ts +359 -0
  88. package/src/__tests__/trace-analyzer.test.ts +52 -2
  89. package/src/cli/commands/CONTEXT.md +3 -2
  90. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +158 -0
  91. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  92. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  93. package/src/cli/commands/__tests__/registry.test.ts +46 -3
  94. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  95. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  96. package/src/cli/commands/__tests__/report.test.ts +0 -20
  97. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  98. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  99. package/src/cli/commands/__tests__/status.test.ts +66 -148
  100. package/src/cli/commands/definitions.ts +1 -32
  101. package/src/cli/commands/passes-gate.ts +0 -3
  102. package/src/cli/commands/performance.ts +13 -27
  103. package/src/cli/commands/report.ts +3 -61
  104. package/src/cli/commands/status.ts +5 -10
  105. package/src/context/CONTEXT.md +2 -3
  106. package/src/context/__tests__/token-budget.test.ts +1 -81
  107. package/src/context/index.ts +2 -7
  108. package/src/context/token-budget.ts +0 -73
  109. package/src/context/types.ts +0 -9
  110. package/src/core/CONTEXT.md +5 -6
  111. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  112. package/src/core/constraints/checker.ts +33 -2
  113. package/src/core/constraints/git-evidence.ts +6 -3
  114. package/src/core/index.ts +0 -12
  115. package/src/core/spec/__tests__/validator.test.ts +24 -0
  116. package/src/core/spec/validator.ts +9 -10
  117. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  118. package/src/core/validators/index.ts +0 -2
  119. package/src/core/validators/passes-gate.ts +3 -152
  120. package/src/failure/CONTEXT.md +8 -6
  121. package/src/failure/index.ts +0 -10
  122. package/src/gates/CONTEXT.md +3 -2
  123. package/src/gates/__tests__/performance.test.ts +6 -74
  124. package/src/gates/definitions.ts +0 -4
  125. package/src/gates/performance.ts +9 -111
  126. package/src/gates/types.ts +2 -8
  127. package/src/index.ts +1 -37
  128. package/src/knowledge/CONTEXT.md +1 -2
  129. package/src/knowledge/index.ts +0 -1
  130. package/src/monitoring/CONTEXT.md +2 -2
  131. package/src/monitoring/trace-analyzer.ts +160 -126
  132. package/src/types/passes-gate.ts +0 -43
  133. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  134. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  135. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  136. package/dist/cli/commands/analyze-sessions.js +0 -290
  137. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  138. package/dist/cli/commands/update-user-model.d.ts +0 -30
  139. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  140. package/dist/cli/commands/update-user-model.js +0 -406
  141. package/dist/cli/commands/update-user-model.js.map +0 -1
  142. package/dist/cli/session-mining/corrections.d.ts +0 -17
  143. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  144. package/dist/cli/session-mining/corrections.js +0 -58
  145. package/dist/cli/session-mining/corrections.js.map +0 -1
  146. package/dist/cli/session-mining/index.d.ts +0 -13
  147. package/dist/cli/session-mining/index.d.ts.map +0 -1
  148. package/dist/cli/session-mining/index.js +0 -29
  149. package/dist/cli/session-mining/index.js.map +0 -1
  150. package/dist/cli/session-mining/text.d.ts +0 -31
  151. package/dist/cli/session-mining/text.d.ts.map +0 -1
  152. package/dist/cli/session-mining/text.js +0 -126
  153. package/dist/cli/session-mining/text.js.map +0 -1
  154. package/dist/cli/session-mining/transcript.d.ts +0 -34
  155. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  156. package/dist/cli/session-mining/transcript.js +0 -130
  157. package/dist/cli/session-mining/transcript.js.map +0 -1
  158. package/dist/context/compaction.d.ts +0 -57
  159. package/dist/context/compaction.d.ts.map +0 -1
  160. package/dist/context/compaction.js +0 -165
  161. package/dist/context/compaction.js.map +0 -1
  162. package/dist/core/session/clean-state.d.ts +0 -17
  163. package/dist/core/session/clean-state.d.ts.map +0 -1
  164. package/dist/core/session/clean-state.js +0 -192
  165. package/dist/core/session/clean-state.js.map +0 -1
  166. package/dist/core/session/index.d.ts +0 -6
  167. package/dist/core/session/index.d.ts.map +0 -1
  168. package/dist/core/session/index.js +0 -15
  169. package/dist/core/session/index.js.map +0 -1
  170. package/dist/core/session/startup.d.ts +0 -47
  171. package/dist/core/session/startup.d.ts.map +0 -1
  172. package/dist/core/session/startup.js +0 -263
  173. package/dist/core/session/startup.js.map +0 -1
  174. package/dist/failure/constraint-handler.d.ts +0 -83
  175. package/dist/failure/constraint-handler.d.ts.map +0 -1
  176. package/dist/failure/constraint-handler.js +0 -138
  177. package/dist/failure/constraint-handler.js.map +0 -1
  178. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  179. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  180. package/dist/knowledge/lifecycle-hooks.js +0 -143
  181. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  182. package/dist/types/session.d.ts +0 -113
  183. package/dist/types/session.d.ts.map +0 -1
  184. package/dist/types/session.js +0 -6
  185. package/dist/types/session.js.map +0 -1
  186. package/src/__tests__/clean-state-extra.test.ts +0 -301
  187. package/src/__tests__/clean-state.test.ts +0 -169
  188. package/src/__tests__/constraint-handler.test.ts +0 -132
  189. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  190. package/src/__tests__/session-startup.test.ts +0 -334
  191. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  192. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -268
  193. package/src/cli/commands/analyze-sessions.ts +0 -335
  194. package/src/cli/commands/update-user-model.ts +0 -476
  195. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  196. package/src/cli/session-mining/corrections.ts +0 -56
  197. package/src/cli/session-mining/index.ts +0 -13
  198. package/src/cli/session-mining/text.ts +0 -117
  199. package/src/cli/session-mining/transcript.ts +0 -125
  200. package/src/context/__tests__/compaction.test.ts +0 -169
  201. package/src/context/compaction.ts +0 -201
  202. package/src/core/session/__tests__/startup.test.ts +0 -281
  203. package/src/core/session/clean-state.ts +0 -178
  204. package/src/core/session/index.ts +0 -15
  205. package/src/core/session/startup.ts +0 -253
  206. package/src/failure/constraint-handler.ts +0 -171
  207. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  208. package/src/knowledge/lifecycle-hooks.ts +0 -202
  209. package/src/types/session.ts +0 -132
@@ -35,7 +35,7 @@ describe('performance command', () => {
35
35
  passed: true,
36
36
  message: 'ok',
37
37
  details: {
38
- metrics: { coverage: 85.5, bundleSize: 204800, benchmarkTime: 150 },
38
+ metrics: { coverage: 85.5, bundleSize: 204800 },
39
39
  },
40
40
  });
41
41
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
@@ -76,7 +76,7 @@ describe('performance command', () => {
76
76
  });
77
77
  });
78
78
 
79
- it('should convert bundleThreshold from KB to bytes', async () => {
79
+ it('bundleThreshold 直传 KB maxBundleSize(不再按字节错位换算)', async () => {
80
80
  const mockCheck = jest.fn().mockResolvedValue({ passed: true, message: 'ok' });
81
81
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
82
82
 
@@ -84,23 +84,12 @@ describe('performance command', () => {
84
84
 
85
85
  expect(MockGate).toHaveBeenCalledWith(
86
86
  expect.objectContaining({
87
- thresholds: expect.objectContaining({ bundleSize: 500 * 1024 }),
87
+ thresholds: expect.objectContaining({ maxBundleSize: 500 }),
88
88
  }),
89
89
  );
90
90
  });
91
91
 
92
- it('should convert benchmarkTimeout from seconds to ms', async () => {
93
- const mockCheck = jest.fn().mockResolvedValue({ passed: true, message: 'ok' });
94
- MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
95
-
96
- await performance({ benchmarkTimeout: 30 }, io);
97
-
98
- expect(MockGate).toHaveBeenCalledWith(
99
- expect.objectContaining({ benchmarkTimeout: 30000 }),
100
- );
101
- });
102
-
103
- it('should set coverage threshold when both flags provided', async () => {
92
+ it('coverage 旗帜对齐 minCoverage 字段(原 coverage 错位键名恒不生效)', async () => {
104
93
  const mockCheck = jest.fn().mockResolvedValue({ passed: true, message: 'ok' });
105
94
  MockGate.mockImplementation(() => ({ check: mockCheck }) as any);
106
95
 
@@ -108,7 +97,7 @@ describe('performance command', () => {
108
97
 
109
98
  expect(MockGate).toHaveBeenCalledWith(
110
99
  expect.objectContaining({
111
- thresholds: expect.objectContaining({ coverage: 90 }),
100
+ thresholds: expect.objectContaining({ minCoverage: 90 }),
112
101
  }),
113
102
  );
114
103
  });
@@ -4,7 +4,7 @@
4
4
  * - 定义表内每个实现引用(module+export)都必须可解析(构建/测试期断言,
5
5
  * 取代 commands barrel 的编译期保护——R6 消灭手工同步后的闭环兜底)
6
6
  * - definitions 是纯数据模块:require 它不加载任何命令实现(保 --help/--version 懒加载)
7
- * - 命令面与 CAPABILITIES.md 的 24 顶层命令一致(防误删回归)
7
+ * - 命令面与 CAPABILITIES.md 的 21 顶层命令一致(防误删回归)
8
8
  * - bin/harness.js 端到端冒烟(dist 存在时):--version/--help 零命令实现模块,
9
9
  * 单命令执行只加载该命令模块(O2 per-command 懒加载)
10
10
  */
@@ -35,7 +35,7 @@ function collectRefs(defs: CommandDefinition[]): CommandImplRef[] {
35
35
  const EXPECTED_TOP_LEVEL_COMMANDS = [
36
36
  'check', 'validate', 'passes-gate', 'init', 'report', 'status', 'spec',
37
37
  'sync-docs', 'knowledge', 'sdd', 'failure', 'posteval-plan',
38
- 'update-user-model', 'release', 'analyze-sessions', 'constraints',
38
+ 'release', 'constraints',
39
39
  'spec-baseline-check',
40
40
  // 6 门禁命令(GATE_DEFINITIONS 驱动)
41
41
  'acceptance', 'command', 'contract', 'performance', 'review', 'security',
@@ -88,7 +88,7 @@ describe('命令注册表闭环', () => {
88
88
  }
89
89
  });
90
90
 
91
- it('顶层命令面与预期 24 命令一致(含 6 门禁)', () => {
91
+ it('顶层命令面与预期 21 命令一致(含 6 门禁)', () => {
92
92
  const names = [
93
93
  ...COMMAND_DEFINITIONS.map(d => d.command.split(' ')[0]),
94
94
  ...GATE_DEFINITIONS.map(d => d.cli.command.split(' ')[0]),
@@ -97,6 +97,49 @@ describe('命令注册表闭环', () => {
97
97
  expect(names.sort()).toEqual([...EXPECTED_TOP_LEVEL_COMMANDS].sort());
98
98
  });
99
99
 
100
+ /**
101
+ * 文档手抄面根因闸(review A5)
102
+ *
103
+ * `CLAUDE.md` 的「N CLI subcommands (…)」与 `cli/commands/CONTEXT.md` 的命令清单/计数
104
+ * 都是注册表实面的人工抄本,无闸时必然漂移:#122 机械 −2 后 CLAUDE.md 仍列着早已按
105
+ * ADR-0009 删除的 `doc-freshness-check`,声明 22 而实面 21。三向钉死——计数等于条数、
106
+ * 无幽灵条目、无漏列条目。
107
+ */
108
+ it('CLAUDE.md 与 cli/commands/CONTEXT.md 的命令抄本与定义表实面双向一致', () => {
109
+ const actual = [
110
+ ...COMMAND_DEFINITIONS.map(d => d.command.split(' ')[0]),
111
+ ...GATE_DEFINITIONS.map(d => d.cli.command.split(' ')[0]),
112
+ ].sort();
113
+
114
+ const claudeLine = fs
115
+ .readFileSync(path.join(repoRoot, 'CLAUDE.md'), 'utf-8')
116
+ .split('\n')
117
+ .find(line => line.includes('| `src/cli/commands/` |'));
118
+ expect(claudeLine).toBeDefined();
119
+ const claudeMatch = (claudeLine || '').match(/(\d+) CLI subcommands \(([^)]*)\)/);
120
+ expect(claudeMatch).toBeDefined();
121
+ const claudeNames = claudeMatch![2]
122
+ .split(',')
123
+ .map(name => name.trim())
124
+ .filter(Boolean)
125
+ .sort();
126
+ expect(Number(claudeMatch![1])).toBe(actual.length);
127
+ expect(claudeNames).toEqual(actual);
128
+
129
+ const contextDoc = fs.readFileSync(path.join(repoCommandsDir, 'CONTEXT.md'), 'utf-8');
130
+ const contextCount = contextDoc.match(/(\d+) 个顶层命令/);
131
+ expect(contextCount).toBeDefined();
132
+ expect(Number(contextCount![1])).toBe(actual.length);
133
+ const contextNames = [
134
+ ...(contextDoc.match(/各命令文件:([^\n]+)/)?.[1] || '').split('/'),
135
+ ...(contextDoc.match(/门禁命令实现在 ([^(]+)(/)?.[1] || '').split('/'),
136
+ ]
137
+ .map(name => name.replace(/`/g, '').trim())
138
+ .filter(Boolean)
139
+ .sort();
140
+ expect(contextNames).toEqual(actual);
141
+ });
142
+
100
143
  it('命令定义无重复注册名', () => {
101
144
  const names = COMMAND_DEFINITIONS.map(d => d.command);
102
145
  expect(new Set(names).size).toBe(names.length);
@@ -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
+ });