@dommaker/harness 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +0 -2
  3. package/dist/cli/commands/check.d.ts.map +1 -1
  4. package/dist/cli/commands/check.js +28 -0
  5. package/dist/cli/commands/check.js.map +1 -1
  6. package/dist/cli/commands/definitions.d.ts.map +1 -1
  7. package/dist/cli/commands/definitions.js +1 -39
  8. package/dist/cli/commands/definitions.js.map +1 -1
  9. package/dist/cli/commands/knowledge.d.ts +0 -19
  10. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  11. package/dist/cli/commands/knowledge.js +0 -105
  12. package/dist/cli/commands/knowledge.js.map +1 -1
  13. package/dist/cli/commands/passes-gate.d.ts +0 -2
  14. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  15. package/dist/cli/commands/passes-gate.js +0 -1
  16. package/dist/cli/commands/passes-gate.js.map +1 -1
  17. package/dist/cli/commands/performance.d.ts +0 -4
  18. package/dist/cli/commands/performance.d.ts.map +1 -1
  19. package/dist/cli/commands/performance.js +10 -20
  20. package/dist/cli/commands/performance.js.map +1 -1
  21. package/dist/cli/commands/report.d.ts +1 -1
  22. package/dist/cli/commands/report.d.ts.map +1 -1
  23. package/dist/cli/commands/report.js +2 -55
  24. package/dist/cli/commands/report.js.map +1 -1
  25. package/dist/cli/commands/status.d.ts.map +1 -1
  26. package/dist/cli/commands/status.js +4 -8
  27. package/dist/cli/commands/status.js.map +1 -1
  28. package/dist/context/index.d.ts +2 -5
  29. package/dist/context/index.d.ts.map +1 -1
  30. package/dist/context/index.js +2 -8
  31. package/dist/context/index.js.map +1 -1
  32. package/dist/context/token-budget.d.ts +0 -29
  33. package/dist/context/token-budget.d.ts.map +1 -1
  34. package/dist/context/token-budget.js +1 -65
  35. package/dist/context/token-budget.js.map +1 -1
  36. package/dist/context/types.d.ts +0 -1
  37. package/dist/context/types.d.ts.map +1 -1
  38. package/dist/context/types.js +0 -9
  39. package/dist/context/types.js.map +1 -1
  40. package/dist/core/constraints/checker.d.ts +14 -0
  41. package/dist/core/constraints/checker.d.ts.map +1 -1
  42. package/dist/core/constraints/checker.js +27 -5
  43. package/dist/core/constraints/checker.js.map +1 -1
  44. package/dist/core/constraints/checkers/capability-sync.d.ts +17 -6
  45. package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
  46. package/dist/core/constraints/checkers/capability-sync.js +48 -19
  47. package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
  48. package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
  49. package/dist/core/constraints/checkers/docs-freshness.js +15 -6
  50. package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
  51. package/dist/core/constraints/checkers/index.d.ts +2 -2
  52. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  53. package/dist/core/constraints/checkers/index.js +2 -1
  54. package/dist/core/constraints/checkers/index.js.map +1 -1
  55. package/dist/core/constraints/checkers/types.d.ts +41 -2
  56. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  57. package/dist/core/constraints/checkers/types.js +33 -0
  58. package/dist/core/constraints/checkers/types.js.map +1 -1
  59. package/dist/core/constraints/git-evidence.d.ts +6 -3
  60. package/dist/core/constraints/git-evidence.d.ts.map +1 -1
  61. package/dist/core/constraints/git-evidence.js +6 -3
  62. package/dist/core/constraints/git-evidence.js.map +1 -1
  63. package/dist/core/index.d.ts +1 -2
  64. package/dist/core/index.d.ts.map +1 -1
  65. package/dist/core/index.js +1 -9
  66. package/dist/core/index.js.map +1 -1
  67. package/dist/core/spec/validator.d.ts +2 -0
  68. package/dist/core/spec/validator.d.ts.map +1 -1
  69. package/dist/core/spec/validator.js +9 -11
  70. package/dist/core/spec/validator.js.map +1 -1
  71. package/dist/core/validators/index.d.ts +1 -1
  72. package/dist/core/validators/index.d.ts.map +1 -1
  73. package/dist/core/validators/passes-gate.d.ts +1 -15
  74. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  75. package/dist/core/validators/passes-gate.js +0 -125
  76. package/dist/core/validators/passes-gate.js.map +1 -1
  77. package/dist/failure/index.d.ts +0 -1
  78. package/dist/failure/index.d.ts.map +1 -1
  79. package/dist/failure/index.js +1 -7
  80. package/dist/failure/index.js.map +1 -1
  81. package/dist/gates/checker-gate.d.ts +2 -1
  82. package/dist/gates/checker-gate.d.ts.map +1 -1
  83. package/dist/gates/checker-gate.js +10 -7
  84. package/dist/gates/checker-gate.js.map +1 -1
  85. package/dist/gates/definitions.d.ts.map +1 -1
  86. package/dist/gates/definitions.js +0 -4
  87. package/dist/gates/definitions.js.map +1 -1
  88. package/dist/gates/performance.d.ts +7 -22
  89. package/dist/gates/performance.d.ts.map +1 -1
  90. package/dist/gates/performance.js +7 -89
  91. package/dist/gates/performance.js.map +1 -1
  92. package/dist/gates/types.d.ts +2 -8
  93. package/dist/gates/types.d.ts.map +1 -1
  94. package/dist/index.d.ts +6 -8
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +4 -22
  97. package/dist/index.js.map +1 -1
  98. package/dist/knowledge/index.d.ts +0 -1
  99. package/dist/knowledge/index.d.ts.map +1 -1
  100. package/dist/knowledge/index.js +1 -3
  101. package/dist/knowledge/index.js.map +1 -1
  102. package/dist/monitoring/trace-analyzer.d.ts +25 -10
  103. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  104. package/dist/monitoring/trace-analyzer.js +147 -116
  105. package/dist/monitoring/trace-analyzer.js.map +1 -1
  106. package/dist/types/constraint.d.ts +8 -0
  107. package/dist/types/constraint.d.ts.map +1 -1
  108. package/dist/types/constraint.js +6 -1
  109. package/dist/types/constraint.js.map +1 -1
  110. package/dist/types/passes-gate.d.ts +0 -19
  111. package/dist/types/passes-gate.d.ts.map +1 -1
  112. package/dist/types/trace.d.ts +9 -0
  113. package/dist/types/trace.d.ts.map +1 -1
  114. package/package.json +1 -1
  115. package/src/CONTEXT.md +1 -0
  116. package/src/__tests__/passes-gate-check.test.ts +20 -4
  117. package/src/__tests__/passes-gate.test.ts +18 -119
  118. package/src/__tests__/performance-gate.test.ts +3 -11
  119. package/src/__tests__/public-exports.test.ts +46 -16
  120. package/src/__tests__/trace-analyzer.test.ts +52 -2
  121. package/src/cli/commands/CONTEXT.md +5 -4
  122. package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +6 -0
  123. package/src/cli/commands/__tests__/check.test.ts +74 -0
  124. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +70 -0
  125. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  126. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  127. package/src/cli/commands/__tests__/project-path-convention.test.ts +85 -23
  128. package/src/cli/commands/__tests__/registry.test.ts +52 -3
  129. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  130. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  131. package/src/cli/commands/__tests__/report.test.ts +0 -20
  132. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  133. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  134. package/src/cli/commands/__tests__/status.test.ts +66 -148
  135. package/src/cli/commands/check.ts +39 -1
  136. package/src/cli/commands/definitions.ts +1 -39
  137. package/src/cli/commands/knowledge.ts +0 -119
  138. package/src/cli/commands/passes-gate.ts +0 -3
  139. package/src/cli/commands/performance.ts +13 -27
  140. package/src/cli/commands/report.ts +3 -61
  141. package/src/cli/commands/status.ts +5 -10
  142. package/src/context/CONTEXT.md +2 -3
  143. package/src/context/__tests__/token-budget.test.ts +1 -81
  144. package/src/context/index.ts +2 -7
  145. package/src/context/token-budget.ts +0 -73
  146. package/src/context/types.ts +0 -9
  147. package/src/core/CONTEXT.md +6 -6
  148. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  149. package/src/core/constraints/checker.ts +41 -6
  150. package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +90 -31
  151. package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +176 -0
  152. package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +18 -9
  153. package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +12 -4
  154. package/src/core/constraints/checkers/capability-sync.ts +56 -18
  155. package/src/core/constraints/checkers/docs-freshness.ts +18 -5
  156. package/src/core/constraints/checkers/index.ts +9 -2
  157. package/src/core/constraints/checkers/types.ts +58 -2
  158. package/src/core/constraints/git-evidence.ts +6 -3
  159. package/src/core/index.ts +0 -11
  160. package/src/core/spec/__tests__/validator.test.ts +24 -0
  161. package/src/core/spec/validator.ts +9 -10
  162. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  163. package/src/core/validators/index.ts +0 -1
  164. package/src/core/validators/passes-gate.ts +1 -149
  165. package/src/failure/CONTEXT.md +8 -6
  166. package/src/failure/index.ts +0 -10
  167. package/src/gates/CONTEXT.md +4 -3
  168. package/src/gates/__tests__/checker-gate.test.ts +24 -0
  169. package/src/gates/__tests__/performance.test.ts +6 -74
  170. package/src/gates/checker-gate.ts +11 -15
  171. package/src/gates/definitions.ts +0 -4
  172. package/src/gates/performance.ts +9 -111
  173. package/src/gates/types.ts +2 -8
  174. package/src/index.ts +1 -36
  175. package/src/knowledge/CONTEXT.md +1 -2
  176. package/src/knowledge/index.ts +0 -1
  177. package/src/monitoring/CONTEXT.md +2 -2
  178. package/src/monitoring/trace-analyzer.ts +160 -126
  179. package/src/types/constraint.ts +16 -2
  180. package/src/types/passes-gate.ts +0 -26
  181. package/src/types/trace.ts +10 -0
  182. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  183. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  184. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  185. package/dist/cli/commands/analyze-sessions.js +0 -290
  186. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  187. package/dist/cli/commands/update-user-model.d.ts +0 -25
  188. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  189. package/dist/cli/commands/update-user-model.js +0 -397
  190. package/dist/cli/commands/update-user-model.js.map +0 -1
  191. package/dist/cli/session-mining/corrections.d.ts +0 -17
  192. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  193. package/dist/cli/session-mining/corrections.js +0 -58
  194. package/dist/cli/session-mining/corrections.js.map +0 -1
  195. package/dist/cli/session-mining/index.d.ts +0 -13
  196. package/dist/cli/session-mining/index.d.ts.map +0 -1
  197. package/dist/cli/session-mining/index.js +0 -29
  198. package/dist/cli/session-mining/index.js.map +0 -1
  199. package/dist/cli/session-mining/text.d.ts +0 -31
  200. package/dist/cli/session-mining/text.d.ts.map +0 -1
  201. package/dist/cli/session-mining/text.js +0 -126
  202. package/dist/cli/session-mining/text.js.map +0 -1
  203. package/dist/cli/session-mining/transcript.d.ts +0 -34
  204. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  205. package/dist/cli/session-mining/transcript.js +0 -130
  206. package/dist/cli/session-mining/transcript.js.map +0 -1
  207. package/dist/context/compaction.d.ts +0 -57
  208. package/dist/context/compaction.d.ts.map +0 -1
  209. package/dist/context/compaction.js +0 -165
  210. package/dist/context/compaction.js.map +0 -1
  211. package/dist/core/session/clean-state.d.ts +0 -17
  212. package/dist/core/session/clean-state.d.ts.map +0 -1
  213. package/dist/core/session/clean-state.js +0 -192
  214. package/dist/core/session/clean-state.js.map +0 -1
  215. package/dist/core/session/index.d.ts +0 -6
  216. package/dist/core/session/index.d.ts.map +0 -1
  217. package/dist/core/session/index.js +0 -15
  218. package/dist/core/session/index.js.map +0 -1
  219. package/dist/core/session/startup.d.ts +0 -47
  220. package/dist/core/session/startup.d.ts.map +0 -1
  221. package/dist/core/session/startup.js +0 -263
  222. package/dist/core/session/startup.js.map +0 -1
  223. package/dist/failure/constraint-handler.d.ts +0 -83
  224. package/dist/failure/constraint-handler.d.ts.map +0 -1
  225. package/dist/failure/constraint-handler.js +0 -138
  226. package/dist/failure/constraint-handler.js.map +0 -1
  227. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  228. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  229. package/dist/knowledge/lifecycle-hooks.js +0 -143
  230. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  231. package/dist/types/session.d.ts +0 -113
  232. package/dist/types/session.d.ts.map +0 -1
  233. package/dist/types/session.js +0 -6
  234. package/dist/types/session.js.map +0 -1
  235. package/src/__tests__/clean-state-extra.test.ts +0 -301
  236. package/src/__tests__/clean-state.test.ts +0 -169
  237. package/src/__tests__/constraint-handler.test.ts +0 -132
  238. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  239. package/src/__tests__/session-startup.test.ts +0 -334
  240. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  241. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -234
  242. package/src/cli/commands/analyze-sessions.ts +0 -335
  243. package/src/cli/commands/update-user-model.ts +0 -461
  244. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  245. package/src/cli/session-mining/corrections.ts +0 -56
  246. package/src/cli/session-mining/index.ts +0 -13
  247. package/src/cli/session-mining/text.ts +0 -117
  248. package/src/cli/session-mining/transcript.ts +0 -125
  249. package/src/context/__tests__/compaction.test.ts +0 -169
  250. package/src/context/compaction.ts +0 -201
  251. package/src/core/session/__tests__/startup.test.ts +0 -281
  252. package/src/core/session/clean-state.ts +0 -178
  253. package/src/core/session/index.ts +0 -15
  254. package/src/core/session/startup.ts +0 -253
  255. package/src/failure/constraint-handler.ts +0 -171
  256. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  257. package/src/knowledge/lifecycle-hooks.ts +0 -202
  258. package/src/types/session.ts +0 -132
@@ -1,117 +0,0 @@
1
- /**
2
- * 分词 / 停用词 / 相似度 / 文本清洗(工单 19-C)
3
- *
4
- * 合并 analyze-sessions 与 update-user-model 的重复实现。
5
- */
6
-
7
- /** N-gram 停用词表(中英) */
8
- export const STOP_WORDS = new Set([
9
- '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一',
10
- '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着',
11
- '没有', '看', '好', '自己', '这', '他', '她', '它', '们', '那',
12
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
13
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
14
- 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
15
- 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
16
- 'before', 'after', 'above', 'below', 'between', 'under', 'again',
17
- 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why',
18
- 'how', 'all', 'both', 'each', 'few', 'more', 'most', 'other', 'some',
19
- 'such', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just',
20
- '因为', '所以', '但是', '而且', '如果', '虽然', '可以', '这个', '那个',
21
- '什么', '怎么', '哪', '吗', '吧', '呢', '啊', '嗯', '哦',
22
- ]);
23
-
24
- const CN_CHAR = /[\u4e00-\u9fff\u3400-\u4dbf]/;
25
-
26
- /**
27
- * 中英文混合分词:中文产出 bigram/trigram(短序列保留整体),
28
- * 连续的拉丁/数字/路径字符作为整体 token。
29
- */
30
- export function tokenize(text: string): string[] {
31
- const tokens: string[] = [];
32
- let current = '';
33
-
34
- for (const char of text) {
35
- if (CN_CHAR.test(char)) {
36
- current += char;
37
- } else if (/[a-zA-Z0-9_\-/.]/.test(char)) {
38
- current += char;
39
- } else {
40
- pushToken(current, tokens);
41
- current = '';
42
- }
43
- }
44
- pushToken(current, tokens);
45
-
46
- return tokens;
47
- }
48
-
49
- function pushToken(current: string, tokens: string[]): void {
50
- if (current.length < 2) return;
51
- if (CN_CHAR.test(current)) {
52
- for (let i = 0; i <= current.length - 2; i++) {
53
- tokens.push(current.slice(i, i + 2));
54
- }
55
- for (let i = 0; i <= current.length - 3; i++) {
56
- tokens.push(current.slice(i, i + 3));
57
- }
58
- if (current.length <= 6) tokens.push(current);
59
- } else {
60
- tokens.push(current);
61
- }
62
- }
63
-
64
- /**
65
- * 基于 tokenize 的 Jaccard 相似度
66
- */
67
- export function jaccardSimilarity(a: string, b: string): number {
68
- const setA = new Set(tokenize(a));
69
- const setB = new Set(tokenize(b));
70
- const intersection = new Set([...setA].filter(x => setB.has(x)));
71
- const union = new Set([...setA, ...setB]);
72
- return union.size === 0 ? 0 : intersection.size / union.size;
73
- }
74
-
75
- /**
76
- * 仅取汉字字符集的 Jaccard 相似度(概念聚类用)
77
- */
78
- export function jaccardChinese(a: string, b: string): number {
79
- const charsA = new Set(a.split('').filter(c => /[\u4e00-\u9fff]/.test(c)));
80
- const charsB = new Set(b.split('').filter(c => /[\u4e00-\u9fff]/.test(c)));
81
- if (charsA.size === 0 || charsB.size === 0) return 0;
82
- const intersection = new Set([...charsA].filter(x => charsB.has(x)));
83
- const union = new Set([...charsA, ...charsB]);
84
- return intersection.size / union.size;
85
- }
86
-
87
- /**
88
- * 剥离代码块 / 行内代码 / JSON 片段,保留自然语言
89
- */
90
- export function stripCodeBlocks(text: string): string {
91
- return text
92
- .replace(/```[\s\S]*?```/g, '')
93
- .replace(/`[^`]+`/g, '')
94
- .replace(/\{[^{}]*"[^"]+"\s*:\s*"[^"]*"[^}]*\}/g, '')
95
- .replace(/\[[\s\S]*?\]/g, '')
96
- .replace(/\\n/g, ' ')
97
- .replace(/[{}[\]"':,\\]+/g, ' ');
98
- }
99
-
100
- /** 是否纯标点/空白 */
101
- export function isPunctuation(str: string): boolean {
102
- return /^[,。!?、;:""''()[\]【】「」『』《》〈〉\s]+$/.test(str);
103
- }
104
-
105
- /** 是否含语义内容(汉字,或 ≥7 字符的合法英文标识词) */
106
- export function hasSemanticContent(phrase: string): boolean {
107
- if (/[\u4e00-\u9fff]/.test(phrase)) return true;
108
- if (phrase.length >= 7 && /^[a-zA-Z][a-zA-Z0-9_]*[a-zA-Z]$/.test(phrase)) return true;
109
- return false;
110
- }
111
-
112
- /** 是否代码噪声(短拉丁片段 / 常见 JSON 字段名碎片) */
113
- export function isCodeNoise(phrase: string): boolean {
114
- if (!/[\u4e00-\u9fff]/.test(phrase) && phrase.length <= 6) return true;
115
- if (/^(?:clla|cood|odde|oppe|laaw|peen|enne|ennc|ddec|deco|ecod|code|type|role|name|text|file|path|tool|user|session)$/i.test(phrase)) return true;
116
- return false;
117
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * Claude Code transcript(.jsonl)读取(工单 19-C)
3
- *
4
- * 原 analyze-sessions.findSessions 与 update-user-model.findNewSessions
5
- * 各持一份解析逻辑,收敛于此。
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { readJsonl } from '../../utils/jsonl';
11
-
12
- export interface MinedTurn {
13
- role: string;
14
- content: string;
15
- }
16
-
17
- export interface MinedSession {
18
- /** 会话 ID(文件名去掉 .jsonl,截断至 40 字符) */
19
- id: string;
20
- /** 文件修改日期(YYYY-MM-DD) */
21
- date: string;
22
- /** 修改时间戳(毫秒) */
23
- mtimeMs: number;
24
- turns: MinedTurn[];
25
- /** 出现过的工具调用名(去重,保持出现顺序) */
26
- toolCalls: string[];
27
- }
28
-
29
- function extractText(content: unknown): string {
30
- if (typeof content === 'string') return content;
31
- if (Array.isArray(content)) {
32
- return content
33
- .filter((b: any) => b.type === 'text')
34
- .map((b: any) => b.text)
35
- .join(' ');
36
- }
37
- return '';
38
- }
39
-
40
- /**
41
- * 扫描目录下的 .jsonl transcript 文件
42
- *
43
- * 不可读/损坏的行静默跳过(与原实现一致)。
44
- */
45
- export interface TranscriptFilter {
46
- /** 只读 mtimeMs >= since 的文件(stat 级过滤,命中即不 parse);缺省不过滤 */
47
- since?: number;
48
- /** 按会话 ID(文件名去 .jsonl、截断 40 字符)排除(文件名级过滤,不 stat 不 parse);缺省不排除 */
49
- excludeIds?: string[];
50
- }
51
-
52
- export function readTranscriptSessions(dir: string, filter: TranscriptFilter = {}): MinedSession[] {
53
- const sessions: MinedSession[] = [];
54
- const exclude = filter.excludeIds && filter.excludeIds.length > 0
55
- ? new Set(filter.excludeIds)
56
- : undefined;
57
-
58
- let files: string[];
59
- try {
60
- files = fs.readdirSync(dir);
61
- } catch {
62
- return sessions;
63
- }
64
-
65
- for (const file of files) {
66
- if (!file.endsWith('.jsonl')) continue;
67
- const id = file.replace('.jsonl', '').slice(0, 40);
68
- if (exclude?.has(id)) continue;
69
- const filePath = path.join(dir, file);
70
-
71
- let stat: fs.Stats;
72
- try {
73
- stat = fs.statSync(filePath);
74
- } catch {
75
- continue;
76
- }
77
- if (filter.since !== undefined && stat.mtimeMs < filter.since) continue;
78
-
79
- const turns: MinedTurn[] = [];
80
- const toolCalls: string[] = [];
81
-
82
- let records: Record<string, any>[];
83
- try {
84
- // 损坏行静默跳过(坏行策略 skip,与原实现一致,harness#82 走 jsonl 正本);
85
- // 外层 catch 兜文件不可读,沿用原「跳过该文件」语义
86
- // 计数去向:豁免(harness#100)——读的是外部 Claude Code transcript(非 harness 写入面)。
87
- // 影响方向如实:坏行使该会话少几条 turn,而 analyze-sessions 的候选按次数阈值筛
88
- // (frequency>=4 / 跨会话>=2 / confidence=len/10)→ 只会少出候选,不会造出候选,且候选须人审;
89
- // 告知需改 MinedSession 形状并波及 analyze-sessions/update-user-model 两个调用方,另票收口
90
- records = readJsonl<Record<string, any>>(filePath, 'skip').records;
91
- } catch {
92
- continue;
93
- }
94
-
95
- for (const entry of records) {
96
- try {
97
- const msg = entry.message;
98
- if (msg?.role && (msg.content || entry.type === 'user')) {
99
- turns.push({ role: msg.role, content: extractText(msg.content) });
100
- }
101
- if (msg?.role === 'assistant' && Array.isArray(msg.content)) {
102
- for (const block of msg.content) {
103
- if (block.type === 'tool_use' && block.name) {
104
- toolCalls.push(block.name);
105
- }
106
- }
107
- }
108
- } catch {
109
- // 单条记录提取异常跳过(原逐行 catch 语义:parse 已由 module skip,此处兜提取)
110
- }
111
- }
112
-
113
- if (turns.length > 0) {
114
- sessions.push({
115
- id,
116
- date: stat.mtime.toISOString().slice(0, 10),
117
- mtimeMs: stat.mtimeMs,
118
- turns,
119
- toolCalls: [...new Set(toolCalls)],
120
- });
121
- }
122
- }
123
-
124
- return sessions;
125
- }
@@ -1,169 +0,0 @@
1
- /**
2
- * SessionCompaction 测试
3
- */
4
-
5
- import { SessionCompaction } from '../compaction';
6
- import type { SessionMessage } from '../types';
7
-
8
- function makeMessages(count: number, contentLength: number = 100): SessionMessage[] {
9
- return Array.from({ length: count }, (_, i) => ({
10
- role: (i % 2 === 0 ? 'user' : 'assistant') as 'user' | 'assistant',
11
- content: `message ${i} `.repeat(contentLength / 10),
12
- timestamp: new Date(Date.now() + i * 1000).toISOString(),
13
- }));
14
- }
15
-
16
- describe('SessionCompaction', () => {
17
- describe('shouldCompact', () => {
18
- it('应该返回 true 当超过触发比例', () => {
19
- const compaction = new SessionCompaction({ triggerRatio: 0.5 });
20
- const messages = makeMessages(10, 500);
21
- // 10 * ~50 tokens each ≈ 500 tokens, budget=800 → ratio > 0.5
22
- expect(compaction.shouldCompact(messages, 800)).toBe(true);
23
- });
24
-
25
- it('应该返回 false 当未超过触发比例', () => {
26
- const compaction = new SessionCompaction({ triggerRatio: 0.9 });
27
- const messages = makeMessages(2, 10);
28
- expect(compaction.shouldCompact(messages, 10000)).toBe(false);
29
- });
30
- });
31
-
32
- describe('compact (eviction)', () => {
33
- it('应该按比例丢弃最早消息', () => {
34
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: false });
35
- const messages = makeMessages(20, 500);
36
- const result = compaction.compact(messages, 1000);
37
- expect(result.level).toBe('eviction');
38
- expect(result.compacted.length).toBeLessThan(messages.length);
39
- expect(result.compacted.length).toBeGreaterThanOrEqual(2);
40
- });
41
-
42
- it('应该保留最近的消息', () => {
43
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: false });
44
- const messages = makeMessages(20, 100);
45
- const result = compaction.compact(messages, 1000);
46
- const lastOriginal = messages[messages.length - 1];
47
- expect(result.compacted[result.compacted.length - 1]).toBe(lastOriginal);
48
- });
49
-
50
- it('应该保留 tool-call 边界对', () => {
51
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
52
- const messages: SessionMessage[] = [
53
- { role: 'user', content: 'a'.repeat(1000), timestamp: 't1' },
54
- { role: 'assistant', content: 'b'.repeat(1000), toolCallId: 'call-1', timestamp: 't2' },
55
- { role: 'tool', content: 'c'.repeat(1000), toolCallId: 'call-1', timestamp: 't3' },
56
- { role: 'user', content: 'd'.repeat(1000), timestamp: 't4' },
57
- { role: 'assistant', content: 'e'.repeat(1000), timestamp: 't5' },
58
- ];
59
- const result = compaction.compact(messages, 500);
60
- // 如果 tool result is kept, its paired assistant call should also be kept
61
- const toolResult = result.compacted.find(m => m.role === 'tool');
62
- if (toolResult) {
63
- const paired = result.compacted.find(m => m.toolCallId === toolResult.toolCallId && m.role === 'assistant');
64
- expect(paired).toBeDefined();
65
- }
66
- });
67
- });
68
-
69
- describe('compact (summary)', () => {
70
- it('应该生成结构化摘要', () => {
71
- const compaction = new SessionCompaction({ level: 'summary' });
72
- const messages = makeMessages(20, 100);
73
- const result = compaction.compact(messages, 1000);
74
- expect(result.level).toBe('summary');
75
- expect(result.summary).toBeDefined();
76
- expect(result.summary).toContain('用户目标');
77
- expect(result.summary).toContain('消息总数');
78
- });
79
-
80
- it('应该保留最近 6 条消息', () => {
81
- const compaction = new SessionCompaction({ level: 'summary' });
82
- const messages = makeMessages(20, 100);
83
- const result = compaction.compact(messages, 1000);
84
- // 1 summary message + 6 recent
85
- expect(result.compacted.length).toBe(7);
86
- });
87
- });
88
-
89
- describe('compact (checkpoint)', () => {
90
- it('应该压缩为单条 checkpoint 消息', () => {
91
- const compaction = new SessionCompaction({ level: 'checkpoint' });
92
- const messages = makeMessages(20, 100);
93
- const result = compaction.compact(messages, 1000);
94
- expect(result.level).toBe('checkpoint');
95
- expect(result.compacted.length).toBe(1);
96
- expect(result.compacted[0].content).toContain('Checkpoint');
97
- });
98
- });
99
-
100
- describe('generateStructuredSummary', () => {
101
- it('应该从用户消息提取目标', () => {
102
- const compaction = new SessionCompaction();
103
- const messages: SessionMessage[] = [
104
- { role: 'user', content: '帮我写一个函数', timestamp: 't1' },
105
- { role: 'assistant', content: '好的', timestamp: 't2' },
106
- ];
107
- const summary = compaction.generateStructuredSummary(messages);
108
- expect(summary).toContain('帮我写一个函数');
109
- });
110
-
111
- it('应该统计工具调用', () => {
112
- const compaction = new SessionCompaction();
113
- const messages: SessionMessage[] = [
114
- { role: 'tool', content: 'result1', timestamp: 't1' },
115
- { role: 'tool', content: 'result2', timestamp: 't2' },
116
- ];
117
- const summary = compaction.generateStructuredSummary(messages);
118
- expect(summary).toContain('2 次');
119
- });
120
- });
121
-
122
- describe('getConfig', () => {
123
- it('应该返回配置', () => {
124
- const compaction = new SessionCompaction({ level: 'summary' });
125
- expect(compaction.getConfig().level).toBe('summary');
126
- });
127
- });
128
-
129
- describe('compact (default level)', () => {
130
- it('应该在未知 level 时回退到 eviction', () => {
131
- const compaction = new SessionCompaction({ level: 'eviction' as any, preserveToolCallPairs: false });
132
- // 直接设置一个不会匹配的 level 来触发 default 分支
133
- (compaction as any).config.level = 'unknown_level';
134
- const messages = makeMessages(20, 500);
135
- const result = compaction.compact(messages, 1000);
136
- expect(result.level).toBe('eviction');
137
- });
138
- });
139
-
140
- describe('preserveToolCallPairs 边界情况', () => {
141
- it('应该在 paired assistant 已在 compacted 中时跳过', () => {
142
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
143
- const messages: SessionMessage[] = [
144
- { role: 'user', content: 'a'.repeat(100), timestamp: 't1' },
145
- { role: 'assistant', content: 'b'.repeat(100), toolCallId: 'call-1', timestamp: 't2' },
146
- { role: 'tool', content: 'c'.repeat(100), toolCallId: 'call-1', timestamp: 't3' },
147
- { role: 'user', content: 'd'.repeat(100), timestamp: 't4' },
148
- { role: 'assistant', content: 'e'.repeat(100), timestamp: 't5' },
149
- ];
150
- // budget 足够大,所有消息都会保留
151
- const result = compaction.compact(messages, 100000);
152
- // paired assistant 已在 compacted 中,不应重复添加
153
- const assistantCalls = result.compacted.filter(m => m.toolCallId === 'call-1' && m.role === 'assistant');
154
- expect(assistantCalls.length).toBe(1);
155
- });
156
-
157
- it('应该处理无 toolCallId 的消息', () => {
158
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
159
- const messages: SessionMessage[] = [
160
- { role: 'user', content: 'a'.repeat(1000), timestamp: 't1' },
161
- { role: 'assistant', content: 'b'.repeat(1000), timestamp: 't2' },
162
- { role: 'user', content: 'c'.repeat(1000), timestamp: 't3' },
163
- { role: 'assistant', content: 'd'.repeat(1000), timestamp: 't4' },
164
- ];
165
- const result = compaction.compact(messages, 500);
166
- expect(result.compacted.length).toBeGreaterThan(0);
167
- });
168
- });
169
- });
@@ -1,201 +0,0 @@
1
- /**
2
- * 会话压缩
3
- *
4
- * 三档策略:eviction / summary / checkpoint
5
- * 不调用 LLM,零 token 成本
6
- */
7
-
8
- import { TokenEstimator } from './token-budget';
9
- import type { CompactionConfig, CompactionLevel, SessionMessage } from './types';
10
- import { DEFAULT_COMPACTION_CONFIG } from './types';
11
-
12
- export interface CompactionResult {
13
- compacted: SessionMessage[];
14
- summary?: string;
15
- level: CompactionLevel;
16
- originalTokens: number;
17
- compactedTokens: number;
18
- }
19
-
20
- export class SessionCompaction {
21
- private config: CompactionConfig;
22
-
23
- constructor(config?: Partial<CompactionConfig>) {
24
- this.config = { ...DEFAULT_COMPACTION_CONFIG, ...config };
25
- }
26
-
27
- /**
28
- * 检查是否需要压缩
29
- */
30
- shouldCompact(messages: SessionMessage[], budget: number): boolean {
31
- const totalTokens = this.estimateMessages(messages);
32
- return totalTokens / budget > this.config.triggerRatio;
33
- }
34
-
35
- /**
36
- * 执行压缩
37
- */
38
- compact(messages: SessionMessage[], budget: number): CompactionResult {
39
- const originalTokens = this.estimateMessages(messages);
40
-
41
- switch (this.config.level) {
42
- case 'eviction':
43
- return this.evictionCompact(messages, budget, originalTokens);
44
- case 'summary':
45
- return this.summaryCompact(messages, budget, originalTokens);
46
- case 'checkpoint':
47
- return this.checkpointCompact(messages, budget, originalTokens);
48
- default:
49
- return this.evictionCompact(messages, budget, originalTokens);
50
- }
51
- }
52
-
53
- /**
54
- * 轻量压缩:按比例丢最早消息,保留 tool-call 边界对
55
- */
56
- private evictionCompact(messages: SessionMessage[], budget: number, originalTokens: number): CompactionResult {
57
- const targetTokens = Math.floor(budget * 0.6); // 目标压缩到 60%
58
- const keepRatio = targetTokens / originalTokens;
59
-
60
- // 至少保留最近 2 条
61
- const minKeep = Math.min(2, messages.length);
62
- const keepCount = Math.max(minKeep, Math.floor(messages.length * keepRatio));
63
-
64
- let compacted = messages.slice(-keepCount);
65
-
66
- // 保留 tool-call 边界对
67
- if (this.config.preserveToolCallPairs) {
68
- compacted = this.preserveToolCallPairs(messages, compacted);
69
- }
70
-
71
- const compactedTokens = this.estimateMessages(compacted);
72
-
73
- return {
74
- compacted,
75
- level: 'eviction',
76
- originalTokens,
77
- compactedTokens,
78
- };
79
- }
80
-
81
- /**
82
- * 中等压缩:结构化总结 + 保留最近 N 轮
83
- */
84
- private summaryCompact(messages: SessionMessage[], _budget: number, originalTokens: number): CompactionResult {
85
- const recentCount = Math.min(6, messages.length); // 保留最近 6 条
86
- const recent = messages.slice(-recentCount);
87
- const older = messages.slice(0, -recentCount);
88
-
89
- // 生成结构化摘要
90
- const summary = this.generateStructuredSummary(older);
91
-
92
- // 摘要作为系统消息插入
93
- const summarySessionMessage: SessionMessage = {
94
- role: 'assistant',
95
- content: `[会话摘要]\n${summary}`,
96
- timestamp: new Date().toISOString(),
97
- };
98
-
99
- const compacted = [summarySessionMessage, ...recent];
100
- const compactedTokens = this.estimateMessages(compacted);
101
-
102
- return {
103
- compacted,
104
- summary,
105
- level: 'summary',
106
- originalTokens,
107
- compactedTokens,
108
- };
109
- }
110
-
111
- /**
112
- * 重型压缩:checkpoint + 清空历史
113
- */
114
- private checkpointCompact(messages: SessionMessage[], _budget: number, originalTokens: number): CompactionResult {
115
- const summary = this.generateStructuredSummary(messages);
116
-
117
- const checkpointSessionMessage: SessionMessage = {
118
- role: 'assistant',
119
- content: `[Checkpoint]\n${summary}\n\n[历史已压缩,共 ${messages.length} 条消息]`,
120
- timestamp: new Date().toISOString(),
121
- };
122
-
123
- const compacted = [checkpointSessionMessage];
124
- const compactedTokens = this.estimateMessages(compacted);
125
-
126
- return {
127
- compacted,
128
- summary,
129
- level: 'checkpoint',
130
- originalTokens,
131
- compactedTokens,
132
- };
133
- }
134
-
135
- /**
136
- * 生成结构化摘要(零 LLM 成本)
137
- */
138
- generateStructuredSummary(messages: SessionMessage[]): string {
139
- const sections: string[] = [];
140
-
141
- // 提取用户目标
142
- const userSessionMessages = messages.filter(m => m.role === 'user');
143
- if (userSessionMessages.length > 0) {
144
- sections.push(`用户目标: ${userSessionMessages[0].content.slice(0, 200)}`);
145
- }
146
-
147
- // 提取工具调用
148
- const toolCalls = messages.filter(m => m.role === 'tool');
149
- if (toolCalls.length > 0) {
150
- sections.push(`工具调用: ${toolCalls.length} 次`);
151
- }
152
-
153
- // 提取关键信息
154
- const assistantSessionMessages = messages.filter(m => m.role === 'assistant');
155
- if (assistantSessionMessages.length > 0) {
156
- const lastAssistant = assistantSessionMessages[assistantSessionMessages.length - 1];
157
- sections.push(`最近响应: ${lastAssistant.content.slice(0, 200)}`);
158
- }
159
-
160
- sections.push(`消息总数: ${messages.length}`);
161
- sections.push(`时间范围: ${messages[0]?.timestamp || 'N/A'} → ${messages[messages.length - 1]?.timestamp || 'N/A'}`);
162
-
163
- return sections.join('\n');
164
- }
165
-
166
- /**
167
- * 保留 tool-call 边界对
168
- *
169
- * 如果 tool 结果被保留,对应的 tool 调用也要保留
170
- */
171
- private preserveToolCallPairs(all: SessionMessage[], compacted: SessionMessage[]): SessionMessage[] {
172
- const result: SessionMessage[] = [];
173
-
174
- for (const msg of compacted) {
175
- // 如果是 tool 结果,确保对应的 tool 调用也在
176
- if (msg.role === 'tool' && msg.toolCallId) {
177
- const paired = all.find(m => m.toolCallId === msg.toolCallId && m.role === 'assistant');
178
- if (paired && !compacted.some(m => m === paired)) {
179
- result.push(paired);
180
- }
181
- }
182
- result.push(msg);
183
- }
184
-
185
- return result;
186
- }
187
-
188
- /**
189
- * 估算消息的 token 数
190
- */
191
- private estimateMessages(messages: SessionMessage[]): number {
192
- return messages.reduce((sum, m) => sum + TokenEstimator.estimateText(m.content), 0);
193
- }
194
-
195
- /**
196
- * 获取配置
197
- */
198
- getConfig(): CompactionConfig {
199
- return { ...this.config };
200
- }
201
- }