@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
@@ -1,268 +0,0 @@
1
- /**
2
- * update-user-model 命令测试(O1:--days flag + --json/--dry-run 兼容)
3
- *
4
- * Seam:updateUserModel(options) 公开入口。
5
- * 隔离面:
6
- * - os.homedir → 测试临时目录(state/profile 落 TEST_HOME,不触碰真实 ~/.claude)
7
- * - readTranscriptSessions → fixture 会话(extractCorrectionMatches 等纯函数保持真实)
8
- */
9
-
10
- import * as fs from 'fs';
11
- import { captureIO, lastJsonOutput, type CapturingIO } from '../../command-contract';
12
- import * as path from 'path';
13
- import { updateUserModel, resolveUserModelPaths } from '../update-user-model';
14
- import { readTranscriptSessions, type MinedSession } from '../../session-mining';
15
-
16
- const TEST_HOME = '/tmp/harness-uum-test-home';
17
-
18
- jest.mock('os', () => ({
19
- ...jest.requireActual('os'),
20
- homedir: () => '/tmp/harness-uum-test-home',
21
- }));
22
-
23
- jest.mock('../../session-mining', () => ({
24
- ...jest.requireActual('../../session-mining'),
25
- readTranscriptSessions: jest.fn(),
26
- }));
27
-
28
- jest.mock('chalk', () => {
29
- const id = (s: string) => s;
30
- const chalkFn = Object.assign(id, {
31
- gray: id,
32
- blue: id,
33
- green: id,
34
- yellow: id,
35
- cyan: id,
36
- bold: id,
37
- red: id,
38
- });
39
- return {
40
- __esModule: true,
41
- default: chalkFn,
42
- gray: id,
43
- blue: id,
44
- green: id,
45
- yellow: id,
46
- cyan: id,
47
- bold: id,
48
- red: id,
49
- };
50
- });
51
-
52
- const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction<
53
- typeof readTranscriptSessions
54
- >;
55
-
56
- const STATE_FILE = path.join(TEST_HOME, '.claude', 'user-model-state.json');
57
- const PROFILE_FILE = path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory', 'user_profile.md');
58
-
59
- function todayStr(offsetDays = 0): string {
60
- return new Date(Date.now() - offsetDays * 86_400_000).toISOString().slice(0, 10);
61
- }
62
-
63
- function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
64
- return {
65
- id: 'session-1',
66
- date: todayStr(),
67
- mtimeMs: Date.now(),
68
- turns: [
69
- { role: 'user', content: '数据库迁移方案需要执行' },
70
- { role: 'assistant', content: '好的' },
71
- ],
72
- toolCalls: ['Read'],
73
- ...partial,
74
- };
75
- }
76
-
77
- let io: CapturingIO;
78
- beforeEach(() => {
79
- io = captureIO();
80
- });
81
-
82
- describe('update-user-model command', () => {
83
-
84
- beforeEach(() => {
85
- jest.clearAllMocks();
86
- fs.rmSync(TEST_HOME, { recursive: true, force: true });
87
- fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
88
- process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
89
- });
90
-
91
- afterEach(() => {
92
- delete process.env.CLAUDE_TRANSCRIPTS_DIR;
93
- delete process.env.HARNESS_UUM_STATE_FILE;
94
- delete process.env.HARNESS_UUM_PROFILE_FILE;
95
- });
96
-
97
- // 路径解析(harness#116):默认值保持原行为,env 覆盖生效
98
- test('resolveUserModelPaths 默认:homedir 下的原路径(现行为不变)', () => {
99
- expect(resolveUserModelPaths({})).toEqual({
100
- stateFile: STATE_FILE,
101
- profileFile: PROFILE_FILE,
102
- });
103
- });
104
-
105
- test('resolveUserModelPaths env 覆盖:HARNESS_UUM_STATE_FILE / HARNESS_UUM_PROFILE_FILE', () => {
106
- expect(resolveUserModelPaths({
107
- HARNESS_UUM_STATE_FILE: '/custom/state.json',
108
- HARNESS_UUM_PROFILE_FILE: '/custom/profile.md',
109
- })).toEqual({
110
- stateFile: '/custom/state.json',
111
- profileFile: '/custom/profile.md',
112
- });
113
- });
114
-
115
- test('env 覆盖端到端:state 落 env 指定路径,不写默认路径', async () => {
116
- const customState = path.join(TEST_HOME, 'custom', 'state.json');
117
- process.env.HARNESS_UUM_STATE_FILE = customState;
118
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
119
-
120
- await updateUserModel({}, io);
121
-
122
- expect(fs.existsSync(customState)).toBe(true);
123
- expect(fs.existsSync(STATE_FILE)).toBe(false);
124
- const state = JSON.parse(fs.readFileSync(customState, 'utf-8'));
125
- expect(state.sessionsProcessed).toContain('session-a');
126
- });
127
-
128
- test('无新会话:提示 No new sessions to process', async () => {
129
- mockReadTranscriptSessions.mockReturnValue([]);
130
-
131
- await updateUserModel({}, io);
132
-
133
- expect(io.outText()).toContain('No new sessions to process');
134
- expect(fs.existsSync(STATE_FILE)).toBe(false);
135
- });
136
-
137
- test('--json:输出 newSessions 与 new_pattern 变化', async () => {
138
- mockReadTranscriptSessions.mockReturnValue([
139
- mkSession({ id: 'session-a' }),
140
- mkSession({ id: 'session-b' }),
141
- ]);
142
-
143
- await updateUserModel({ json: true, dryRun: true }, io);
144
-
145
- const output = lastJsonOutput(io);
146
- expect(output.newSessions).toBe(2);
147
- expect(output.changes).toEqual(
148
- expect.arrayContaining([
149
- expect.objectContaining({
150
- type: 'new_pattern',
151
- key: '数据库迁移方案需要执行',
152
- }),
153
- ]),
154
- );
155
- });
156
-
157
- test('--dry-run:只展示变化,不写 state 文件', async () => {
158
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
159
-
160
- await updateUserModel({ dryRun: true }, io);
161
-
162
- expect(io.outText()).toContain('Processed 1 new sessions');
163
- expect(fs.existsSync(STATE_FILE)).toBe(false);
164
- });
165
-
166
- test('默认(非 dry-run):落盘 state 并记录已处理会话', async () => {
167
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
168
-
169
- await updateUserModel({}, io);
170
-
171
- expect(fs.existsSync(STATE_FILE)).toBe(true);
172
- const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
173
- expect(state.sessionsProcessed).toContain('session-a');
174
- });
175
-
176
- test('--days 1:since 过滤下推到 seam(自然日窗口,含今天)', async () => {
177
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'today' })]);
178
-
179
- await updateUserModel({ json: true, dryRun: true, days: 1 }, io);
180
-
181
- const output = lastJsonOutput(io);
182
- expect(output.newSessions).toBe(1);
183
- // date >= 今日 ⟺ mtimeMs >= 今日 UTC 零点,过滤在 seam 内 stat 级完成
184
- expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
185
- process.env.CLAUDE_TRANSCRIPTS_DIR,
186
- { excludeIds: [], since: Date.parse(todayStr()) },
187
- );
188
- });
189
-
190
- test('缺省 --days:处理全部未处理会话(向后兼容)', async () => {
191
- mockReadTranscriptSessions.mockReturnValue([
192
- mkSession({ id: 'today' }),
193
- mkSession({
194
- id: 'three-days-ago',
195
- date: todayStr(3),
196
- turns: [
197
- { role: 'user', content: '旧会话概念内容测试' },
198
- { role: 'assistant', content: '' },
199
- ],
200
- }),
201
- ]);
202
-
203
- await updateUserModel({ json: true, dryRun: true }, io);
204
-
205
- const output = lastJsonOutput(io);
206
- expect(output.newSessions).toBe(2);
207
- // 缺省 days:不下推 since(向后兼容全量)
208
- expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
209
- process.env.CLAUDE_TRANSCRIPTS_DIR,
210
- { excludeIds: [] },
211
- );
212
- });
213
-
214
- test('同一 concept 只计一次:occurrences 等于 merged 聚合值(#113 双计 bug)', async () => {
215
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
216
-
217
- await updateUserModel({}, io);
218
-
219
- const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
220
- // userText 整条中文序列恰为一个 concept(count=1);聚合后只应出现一次,
221
- // 双计 bug 下会被 merged 循环 + per-session 循环各加一次变成 2
222
- expect(state.patterns['数据库迁移方案需要执行'].occurrences).toBe(1);
223
- expect(state.patterns['数据库迁移方案需要执行'].sessions).toEqual(['session-a']);
224
- });
225
-
226
- test('correction phrase 的 ×3 加权不受影响(buildMergedConcepts 内)', async () => {
227
- mockReadTranscriptSessions.mockReturnValue([
228
- mkSession({
229
- id: 'session-a',
230
- turns: [
231
- { role: 'user', content: '这种问题反复出现' },
232
- { role: 'assistant', content: '' },
233
- ],
234
- }),
235
- ]);
236
-
237
- await updateUserModel({}, io);
238
-
239
- const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
240
- // 纠正语句提取的 concept 按 ×3 计入(裁决:权重在 buildMergedConcepts,删循环不影响)
241
- expect(state.patterns['这种问题'].occurrences).toBe(3);
242
- });
243
-
244
- test('sessionsProcessed 去重:已处理会话经 excludeIds 下推排除', async () => {
245
- fs.writeFileSync(
246
- STATE_FILE,
247
- JSON.stringify({
248
- lastUpdated: '',
249
- sessionsProcessed: ['session-a'],
250
- patterns: {},
251
- lensWeights: {},
252
- principleWeights: {},
253
- evolutionLog: [],
254
- }),
255
- 'utf-8',
256
- );
257
- mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-b' })]);
258
-
259
- await updateUserModel({ json: true, dryRun: true, days: 7 }, io);
260
-
261
- const output = lastJsonOutput(io);
262
- expect(output.newSessions).toBe(1);
263
- expect(mockReadTranscriptSessions).toHaveBeenCalledWith(
264
- process.env.CLAUDE_TRANSCRIPTS_DIR,
265
- { excludeIds: ['session-a'], since: expect.any(Number) },
266
- );
267
- });
268
- });
@@ -1,335 +0,0 @@
1
- /**
2
- * harness analyze-sessions — 对话模式发现引擎
3
- *
4
- * 扫描 transcript 文件,不预设关键词,挖掘:
5
- * 1. 纠正信号(用户反复指出的问题)
6
- * 2. 高频 N-gram(跨会话重复出现的概念)
7
- * 3. 规则缺口(高频模式未被现有 memory 覆盖)
8
- *
9
- * 输出候选规则建议,供用户审核后写入 memory。
10
- *
11
- * 工单 19-C:transcript 解析/纠正模式/分词相似度收敛至 cli/session-mining/。
12
- */
13
-
14
- import chalk from 'chalk';
15
- import * as fs from 'fs';
16
- import * as path from 'path';
17
- import * as os from 'os';
18
- import {
19
- readTranscriptSessions,
20
- type MinedSession,
21
- extractCorrectionMatches,
22
- cleanCorrectionConcept,
23
- tokenize,
24
- stripCodeBlocks,
25
- jaccardSimilarity,
26
- isPunctuation,
27
- isCodeNoise,
28
- hasSemanticContent,
29
- STOP_WORDS,
30
- } from '../session-mining';
31
- import { log, processIO, type CommandIO, type CommandResult } from '../command-contract';
32
-
33
- export interface AnalyzeSessionsOptions {
34
- days?: number;
35
- projectPath?: string;
36
- json?: boolean;
37
- }
38
-
39
- interface Correction {
40
- sentence: string;
41
- sessionId: string;
42
- timestamp: string;
43
- }
44
-
45
- interface PatternCandidate {
46
- pattern: string; // 被纠正/重复的概念
47
- frequency: number; // 跨会话出现次数
48
- sessions: string[]; // 出现的会话 ID
49
- source: 'correction' | 'ngram'; // 来源
50
- confidence: number; // 0-1
51
- suggestedRule: string; // 建议的规则名
52
- }
53
-
54
- // ── Main ──
55
-
56
- export async function analyzeSessions(options: AnalyzeSessionsOptions, io: CommandIO = processIO): Promise<CommandResult> {
57
- const transcriptDir = process.env.CLAUDE_TRANSCRIPTS_DIR
58
- || path.join(os.homedir(), '.claude', 'projects', '-root--claude');
59
- const memoryDir = path.join(os.homedir(), '.claude', 'projects', '-root-projects', 'memory');
60
- const days = options.days || 7;
61
-
62
- if (!fs.existsSync(transcriptDir)) {
63
- log(io, chalk.yellow('No transcripts directory found'));
64
- return { kind: 'skip', reason: `会话记录目录不存在: ${transcriptDir}` };
65
- }
66
-
67
- // 1. Scan transcripts(since 下推 seam(harness#112):stat 级过滤,窗口外不 parse;倒序)
68
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
69
- const sessions = readTranscriptSessions(transcriptDir, { since: cutoff })
70
- .sort((a, b) => b.date.localeCompare(a.date));
71
-
72
- if (sessions.length === 0) {
73
- log(io, chalk.yellow(`No sessions found in the last ${days} days`));
74
- return { kind: 'skip', reason: `最近 ${days} 天没有会话` };
75
- }
76
-
77
- // 2. Extract corrections + N-grams
78
- const corrections = extractCorrections(sessions);
79
- const ngrams = extractNGrams(sessions, { minLen: 4, maxLen: 24, minFreq: 2 });
80
-
81
- // 3. Load existing memory rules for gap detection
82
- const existingRules = loadExistingRules(memoryDir);
83
-
84
- // 4. Generate candidates
85
- const candidates = generateCandidates(corrections, ngrams, existingRules);
86
-
87
- // 5. Output
88
- if (options.json) {
89
- log(io, JSON.stringify({ sessions: sessions.length, corrections: corrections.length, candidates }, null, 2));
90
- return { kind: 'ok' };
91
- }
92
-
93
- log(io, chalk.blue(`🔍 Analyzing ${sessions.length} sessions (last ${days} days)...\n`));
94
-
95
- printCorrectionSummary(corrections, io);
96
- printCandidates(candidates, io);
97
- return { kind: 'ok' };
98
- }
99
-
100
- // ── Correction Extraction ──
101
-
102
- function extractCorrections(sessions: MinedSession[]): Correction[] {
103
- const corrections: Correction[] = [];
104
-
105
- for (const session of sessions) {
106
- for (const turn of session.turns) {
107
- if (turn.role !== 'user') continue;
108
- const text = stripCodeBlocks(turn.content);
109
-
110
- for (const sentence of extractCorrectionMatches(text)) {
111
- if (sentence.length > 3 && sentence.length < 200) {
112
- corrections.push({
113
- sentence,
114
- sessionId: session.id,
115
- timestamp: session.date,
116
- });
117
- }
118
- }
119
- }
120
- }
121
-
122
- return corrections;
123
- }
124
-
125
- // ── N-gram Mining ──
126
-
127
- interface NGramOptions {
128
- minLen: number;
129
- maxLen: number;
130
- minFreq: number;
131
- }
132
-
133
- interface NGramResult {
134
- phrase: string;
135
- frequency: number;
136
- sessions: string[];
137
- }
138
-
139
- function extractNGrams(sessions: MinedSession[], opts: NGramOptions): NGramResult[] {
140
- const phraseMap = new Map<string, { count: number; sessions: Set<string> }>();
141
-
142
- for (const session of sessions) {
143
- const userTexts = session.turns
144
- .filter(t => t.role === 'user' && !t.content.startsWith('{') && !t.content.startsWith('[') && t.content.length > 10)
145
- .map(t => stripCodeBlocks(t.content));
146
-
147
- const combined = userTexts.join(' ');
148
-
149
- // Extract meaningful word sequences (Chinese chars + meaningful words)
150
- const words = tokenize(combined);
151
-
152
- for (let n = 2; n <= 5; n++) {
153
- for (let i = 0; i <= words.length - n; i++) {
154
- const phrase = words.slice(i, i + n).join('');
155
- const clean = phrase.replace(/[,。!?、;:""''()\s]/g, '');
156
-
157
- if (clean.length < opts.minLen || clean.length > opts.maxLen) continue;
158
- if (/^\d+$/.test(clean)) continue;
159
- if (STOP_WORDS.has(clean)) continue;
160
- if (isPunctuation(clean)) continue;
161
-
162
- const existing = phraseMap.get(clean) || { count: 0, sessions: new Set() };
163
- existing.count++;
164
- existing.sessions.add(session.id);
165
- phraseMap.set(clean, existing);
166
- }
167
- }
168
- }
169
-
170
- // Filter by minimum frequency and sort
171
- return Array.from(phraseMap.entries())
172
- .filter(([, v]) => v.count >= opts.minFreq && v.sessions.size >= 2)
173
- .map(([phrase, v]) => ({ phrase, frequency: v.count, sessions: Array.from(v.sessions) }))
174
- .filter(r => !isCodeNoise(r.phrase) && hasSemanticContent(r.phrase))
175
- .sort((a, b) => b.frequency - a.frequency || b.sessions.length - a.sessions.length);
176
- }
177
-
178
- // ── Existing Rules ──
179
-
180
- function isInExistingRules(concept: string, existingRules: Set<string>): boolean {
181
- const lower = concept.toLowerCase();
182
- if (existingRules.has(lower)) return true;
183
- // Partial match: concept appears in any rule name
184
- for (const rule of existingRules) {
185
- if (rule.includes(lower) || lower.includes(rule)) return true;
186
- }
187
- return false;
188
- }
189
-
190
- function loadExistingRules(memoryDir: string): Set<string> {
191
- const concepts = new Set<string>();
192
- try {
193
- if (!fs.existsSync(memoryDir)) return concepts;
194
- for (const file of fs.readdirSync(memoryDir)) {
195
- if (!file.endsWith('.md')) continue;
196
- const content = fs.readFileSync(path.join(memoryDir, file), 'utf-8');
197
- // Extract key concepts from rule titles
198
- const titleMatch = content.match(/#\s+(.+)/);
199
- if (titleMatch) {
200
- const words = tokenize(titleMatch[1]);
201
- for (const w of words) {
202
- if (w.length >= 4) concepts.add(w.toLowerCase());
203
- }
204
- }
205
- }
206
- } catch {}
207
- return concepts;
208
- }
209
-
210
- // ── Candidate Generation ──
211
-
212
- function generateCandidates(
213
- corrections: Correction[],
214
- ngrams: NGramResult[],
215
- existingRules: Set<string>,
216
- ): PatternCandidate[] {
217
- const candidates: PatternCandidate[] = [];
218
-
219
- // From corrections: cluster similar sentences
220
- const correctionClusters = clusterCorrections(corrections);
221
- for (const cluster of correctionClusters) {
222
- // Must appear at least 3 times total
223
- if (cluster.sentences.length < 3) continue;
224
- const concept = extractConcept(cluster.sentences);
225
- if (isInExistingRules(concept, existingRules)) continue;
226
-
227
- candidates.push({
228
- pattern: concept,
229
- frequency: cluster.sentences.length,
230
- sessions: cluster.sessions,
231
- source: 'correction',
232
- confidence: Math.min(1, cluster.sentences.length / 10),
233
- suggestedRule: `feedback_auto_${concept.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '_').slice(0, 40)}.md`,
234
- });
235
- }
236
-
237
- // From N-grams: high-frequency concepts not in corrections
238
- const correctionConcepts = new Set(correctionClusters.map(c => extractConcept(c.sentences).toLowerCase()));
239
- for (const ng of ngrams) {
240
- const concept = ng.phrase;
241
- if (correctionConcepts.has(concept.toLowerCase())) continue; // already covered
242
- if (isInExistingRules(concept, existingRules)) continue;
243
- if (ng.frequency < 2) continue;
244
- const crossSessionStrength = ng.sessions.length >= 2;
245
- const highFreqSingle = ng.sessions.length === 1 && ng.frequency >= 4;
246
- if (!crossSessionStrength && !highFreqSingle) continue;
247
-
248
- candidates.push({
249
- pattern: concept,
250
- frequency: ng.frequency,
251
- sessions: ng.sessions,
252
- source: 'ngram',
253
- confidence: Math.min(1, ng.frequency / 10),
254
- suggestedRule: `feedback_recurring_${concept.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '_').slice(0, 40)}.md`,
255
- });
256
- }
257
-
258
- return candidates.sort((a, b) => b.confidence - a.confidence);
259
- }
260
-
261
- interface CorrectionCluster {
262
- sentences: string[];
263
- sessions: string[];
264
- }
265
-
266
- function clusterCorrections(corrections: Correction[]): CorrectionCluster[] {
267
- const clusters: CorrectionCluster[] = [];
268
- const used = new Set<number>();
269
-
270
- for (let i = 0; i < corrections.length; i++) {
271
- if (used.has(i)) continue;
272
- const cluster: CorrectionCluster = {
273
- sentences: [corrections[i].sentence],
274
- sessions: [corrections[i].sessionId],
275
- };
276
- used.add(i);
277
-
278
- for (let j = i + 1; j < corrections.length; j++) {
279
- if (used.has(j)) continue;
280
- const similarity = jaccardSimilarity(corrections[i].sentence, corrections[j].sentence);
281
- if (similarity > 0.3) {
282
- cluster.sentences.push(corrections[j].sentence);
283
- if (!cluster.sessions.includes(corrections[j].sessionId)) {
284
- cluster.sessions.push(corrections[j].sessionId);
285
- }
286
- used.add(j);
287
- }
288
- }
289
-
290
- clusters.push(cluster);
291
- }
292
-
293
- return clusters;
294
- }
295
-
296
- function extractConcept(sentences: string[]): string {
297
- // Take the longest sentence and extract the noun phrase after the correction marker
298
- const longest = sentences.reduce((a, b) => a.length > b.length ? a : b, '');
299
- const cleaned = cleanCorrectionConcept(longest);
300
- return cleaned.slice(0, 60) || longest.slice(0, 60);
301
- }
302
-
303
- // ── Output ──
304
-
305
- function printCorrectionSummary(corrections: Correction[], io: CommandIO): void {
306
- if (corrections.length === 0) {
307
- log(io, chalk.green('No correction patterns found'));
308
- return;
309
- }
310
-
311
- log(io, chalk.yellow(`📢 ${corrections.length} correction signals detected\n`));
312
- }
313
-
314
- function printCandidates(candidates: PatternCandidate[], io: CommandIO): void {
315
- if (candidates.length === 0) {
316
- log(io, chalk.green('✅ No new pattern candidates — all recurring concepts already have rules\n'));
317
- return;
318
- }
319
-
320
- log(io, chalk.bold('🔔 Pattern Candidates (suggested rules)\n'));
321
-
322
- const topK = candidates.slice(0, 10);
323
- for (const c of topK) {
324
- const icon = c.source === 'correction' ? '🔴' : '🟡';
325
- const confBar = '█'.repeat(Math.round(c.confidence * 10)) + '░'.repeat(10 - Math.round(c.confidence * 10));
326
- log(io, chalk.bold(`${icon} ${c.pattern}`));
327
- log(io, chalk.gray(` Source: ${c.source} | Freq: ${c.frequency} | Sessions: ${c.sessions.length} | Confidence: ${confBar}`));
328
- log(io, chalk.cyan(` → Rule: ${c.suggestedRule}`));
329
- log(io);
330
- }
331
-
332
- if (candidates.length > 10) {
333
- log(io, chalk.gray(`... and ${candidates.length - 10} more candidates. Run with --json for full list.`));
334
- }
335
- }