@morningljn/mnemo 0.2.0 → 0.2.1
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.
- package/dist/dream.d.ts +2 -0
- package/dist/dream.js +20 -0
- package/dist/dream.js.map +1 -0
- package/dist/init.js +4 -24
- package/dist/init.js.map +1 -1
- package/dist/resources.d.ts +22 -8
- package/dist/resources.js +66 -20
- package/dist/resources.js.map +1 -1
- package/dist/retriever.js +12 -5
- package/dist/retriever.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +2 -2
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +22 -1
- package/dist/store.js +145 -4
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +27 -1
- package/docs/superpowers/plans/2026-05-16-memory-dreaming.md +626 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/design.md +71 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/proposal.md +32 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/specs/compact-search/spec.md +16 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/specs/dream-cycle/spec.md +38 -0
- package/openspec/changes/archive/2026-05-16-memory-dreaming/tasks.md +27 -0
- package/openspec/specs/compact-search/spec.md +16 -0
- package/openspec/specs/dream-cycle/spec.md +38 -0
- package/package.json +3 -2
- package/src/dream.ts +20 -0
- package/src/init.ts +4 -24
- package/src/resources.ts +77 -21
- package/src/retriever.ts +9 -5
- package/src/schema.ts +2 -2
- package/src/server.ts +46 -7
- package/src/store.ts +166 -5
- package/src/types.ts +25 -1
- package/tests/resource.test.ts +25 -23
- package/tests/store.test.ts +129 -2
package/src/store.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
import Database from 'better-sqlite3'
|
|
13
13
|
import type { Statement } from 'better-sqlite3'
|
|
14
14
|
import { mkdirSync } from 'node:fs'
|
|
15
|
-
import { dirname } from 'node:path'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
16
|
import { SCHEMA } from './schema.js'
|
|
17
|
-
import type { Fact, FactCategory } from './types.js'
|
|
17
|
+
import type { Fact, FactCategory, DreamReport } from './types.js'
|
|
18
18
|
|
|
19
19
|
// 信任评分常量
|
|
20
20
|
const HELPFUL_DELTA = 0.05
|
|
@@ -136,10 +136,10 @@ export class MemoryStore {
|
|
|
136
136
|
DROP TRIGGER IF EXISTS facts_ad;
|
|
137
137
|
DROP TRIGGER IF EXISTS facts_au;
|
|
138
138
|
`)
|
|
139
|
-
// 重建 FTS5 虚拟表(含 summary
|
|
139
|
+
// 重建 FTS5 虚拟表(含 summary,使用 trigram tokenizer 支持中文子串)
|
|
140
140
|
this.db.exec(`
|
|
141
141
|
CREATE VIRTUAL TABLE facts_fts
|
|
142
|
-
USING fts5(content, tags, summary, content=facts, content_rowid=fact_id);
|
|
142
|
+
USING fts5(content, tags, summary, content=facts, content_rowid=fact_id, tokenize='trigram');
|
|
143
143
|
`)
|
|
144
144
|
// 重填充
|
|
145
145
|
this.db.exec(`
|
|
@@ -743,7 +743,9 @@ export class MemoryStore {
|
|
|
743
743
|
|
|
744
744
|
// Rate-based adjustment (需要 30+ 次检索)
|
|
745
745
|
if (row.retrieval_count > 30) {
|
|
746
|
-
|
|
746
|
+
// 高频检索保护:检索 >100 次说明被持续需要,feedback 少不代表无用
|
|
747
|
+
const highFrequencyProtected = row.retrieval_count > 100
|
|
748
|
+
if (rate < 0.05 && !highFrequencyProtected) {
|
|
747
749
|
const newTrust = clampTrust(row.trust_score * 0.9)
|
|
748
750
|
this.db.prepare('UPDATE facts SET trust_score = ? WHERE fact_id = ?').run(newTrust, row.fact_id)
|
|
749
751
|
demoted++
|
|
@@ -839,6 +841,165 @@ export class MemoryStore {
|
|
|
839
841
|
}
|
|
840
842
|
}
|
|
841
843
|
|
|
844
|
+
/** Dream 前备份数据库(使用 better-sqlite3 内置备份,安全处理 WAL 模式) */
|
|
845
|
+
async backupDatabase(): Promise<string> {
|
|
846
|
+
const dbDir = dirname(this.db.name)
|
|
847
|
+
const backupDir = join(dbDir, 'backup')
|
|
848
|
+
mkdirSync(backupDir, { recursive: true })
|
|
849
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
850
|
+
const backupPath = join(backupDir, `dream-${timestamp}.db`)
|
|
851
|
+
await this.db.backup(backupPath)
|
|
852
|
+
return backupPath
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** 合并同 category 内 Jaccard > 0.6 的重叠 fact */
|
|
856
|
+
mergeOverlappingFacts(): { merged: number; details: Array<{ kept: number; removed: number; similarity: number }> } {
|
|
857
|
+
return this.db.transaction(() => {
|
|
858
|
+
const categories: FactCategory[] = ['identity', 'coding_style', 'tool_pref', 'workflow', 'general']
|
|
859
|
+
let merged = 0
|
|
860
|
+
const details: Array<{ kept: number; removed: number; similarity: number }> = []
|
|
861
|
+
|
|
862
|
+
for (const cat of categories) {
|
|
863
|
+
const rows = this.db.prepare(
|
|
864
|
+
'SELECT fact_id, content, retrieval_count FROM facts WHERE category = ? ORDER BY trust_score DESC'
|
|
865
|
+
).all(cat) as Array<{ fact_id: number; content: string; retrieval_count: number }>
|
|
866
|
+
|
|
867
|
+
const removed = new Set<number>()
|
|
868
|
+
|
|
869
|
+
for (let i = 0; i < rows.length; i++) {
|
|
870
|
+
if (removed.has(rows[i].fact_id)) continue
|
|
871
|
+
const tokensA = this.tokenizeForDedup(rows[i].content)
|
|
872
|
+
|
|
873
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
874
|
+
if (removed.has(rows[j].fact_id)) continue
|
|
875
|
+
const tokensB = this.tokenizeForDedup(rows[j].content)
|
|
876
|
+
const sim = this.jaccardSimilarity(tokensA, tokensB)
|
|
877
|
+
|
|
878
|
+
if (sim > 0.6) {
|
|
879
|
+
const aHighFreq = rows[i].retrieval_count > 100
|
|
880
|
+
const bHighFreq = rows[j].retrieval_count > 100
|
|
881
|
+
|
|
882
|
+
if (aHighFreq && bHighFreq) continue
|
|
883
|
+
|
|
884
|
+
let keptId: number, removedId: number
|
|
885
|
+
if (bHighFreq) {
|
|
886
|
+
keptId = rows[j].fact_id
|
|
887
|
+
removedId = rows[i].fact_id
|
|
888
|
+
} else {
|
|
889
|
+
keptId = rows[i].fact_id
|
|
890
|
+
removedId = rows[j].fact_id
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
this.removeFact(removedId)
|
|
894
|
+
removed.add(removedId)
|
|
895
|
+
details.push({ kept: keptId, removed: removedId, similarity: Math.round(sim * 100) / 100 })
|
|
896
|
+
merged++
|
|
897
|
+
// 外层 fact 被删除时停止内层循环,避免幽灵操作
|
|
898
|
+
if (removedId === rows[i].fact_id) break
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return { merged, details }
|
|
904
|
+
})()
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** 分类修正:按关键词规则表将误分类的 fact 挪到正确 category */
|
|
908
|
+
reclassifyFacts(): number {
|
|
909
|
+
return this.db.transaction(() => {
|
|
910
|
+
const rules: Array<{ keywords: string[]; target: FactCategory }> = [
|
|
911
|
+
{ keywords: ['角色设定', '暖暖', '身份', '编程女朋友', '暖宝宝'], target: 'identity' },
|
|
912
|
+
{ keywords: ['编码规范', '代码风格', 'pytest', '文件不超过', '方法不超过'], target: 'coding_style' },
|
|
913
|
+
{ keywords: ['工作流', 'OpenSpec', 'writing-plans', 'subagent'], target: 'workflow' },
|
|
914
|
+
{ keywords: ['偏好', 'VS Code', '编辑器', 'IDE', '快捷键'], target: 'tool_pref' },
|
|
915
|
+
]
|
|
916
|
+
|
|
917
|
+
const rows = this.db.prepare(
|
|
918
|
+
'SELECT fact_id, content, category FROM facts'
|
|
919
|
+
).all() as Array<{ fact_id: number; content: string; category: string }>
|
|
920
|
+
|
|
921
|
+
let reclassified = 0
|
|
922
|
+
for (const row of rows) {
|
|
923
|
+
for (const rule of rules) {
|
|
924
|
+
if (rule.target === row.category) continue
|
|
925
|
+
if (rule.keywords.some(kw => row.content.includes(kw))) {
|
|
926
|
+
this.db.prepare("UPDATE facts SET category = ?, updated_at = datetime('now', 'localtime') WHERE fact_id = ?").run(rule.target, row.fact_id)
|
|
927
|
+
reclassified++
|
|
928
|
+
break
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return reclassified
|
|
933
|
+
})()
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** 执行完整 dream cycle:备份 → 压缩 → 合并 → 重分类 → 报告 */
|
|
937
|
+
async runDream(options?: { skipBackup?: boolean }): Promise<DreamReport> {
|
|
938
|
+
if (!options?.skipBackup) {
|
|
939
|
+
await this.backupDatabase()
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const compressed = this.compressLongFacts()
|
|
943
|
+
const mergeResult = this.mergeOverlappingFacts()
|
|
944
|
+
const reclassified = this.reclassifyFacts()
|
|
945
|
+
|
|
946
|
+
const stats = this.db.prepare(`
|
|
947
|
+
SELECT COUNT(*) as total,
|
|
948
|
+
AVG(trust_score) as avg_trust,
|
|
949
|
+
AVG(length(content)) as avg_length
|
|
950
|
+
FROM facts
|
|
951
|
+
`).get() as { total: number; avg_trust: number; avg_length: number }
|
|
952
|
+
|
|
953
|
+
const categories: FactCategory[] = ['identity', 'coding_style', 'tool_pref', 'workflow', 'general']
|
|
954
|
+
const coverage: Record<string, number> = {}
|
|
955
|
+
for (const cat of categories) {
|
|
956
|
+
const row = this.db.prepare('SELECT COUNT(*) as c FROM facts WHERE category = ?').get(cat) as { c: number }
|
|
957
|
+
coverage[cat] = row.c
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
merged: mergeResult.merged,
|
|
962
|
+
compressed,
|
|
963
|
+
reclassified,
|
|
964
|
+
deleted: mergeResult.merged,
|
|
965
|
+
mergeDetails: mergeResult.details,
|
|
966
|
+
health: {
|
|
967
|
+
total: stats.total,
|
|
968
|
+
avg_trust: Math.round((stats.avg_trust ?? 0) * 100) / 100,
|
|
969
|
+
avg_length: Math.round(stats.avg_length ?? 0),
|
|
970
|
+
coverage: coverage as Record<FactCategory, number>,
|
|
971
|
+
},
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** 压缩长 fact:content > 200 字且无 summary 的,自动提取前 2 句作为 summary */
|
|
976
|
+
compressLongFacts(): number {
|
|
977
|
+
const rows = this.db.prepare(
|
|
978
|
+
"SELECT fact_id, content FROM facts WHERE length(content) > 200 AND (summary IS NULL OR summary = '')"
|
|
979
|
+
).all() as Array<{ fact_id: number; content: string }>
|
|
980
|
+
|
|
981
|
+
let compressed = 0
|
|
982
|
+
for (const row of rows) {
|
|
983
|
+
const summary = this.extractSummary(row.content)
|
|
984
|
+
if (summary) {
|
|
985
|
+
this.db.prepare('UPDATE facts SET summary = ? WHERE fact_id = ?').run(summary, row.fact_id)
|
|
986
|
+
compressed++
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return compressed
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** 从 content 提取前 2 个完整句子(总长 ≤ 150 字) */
|
|
993
|
+
private extractSummary(content: string): string | null {
|
|
994
|
+
const sentences = content.split(/[。\n.]/).map(s => s.trim()).filter(s => s.length > 0)
|
|
995
|
+
if (sentences.length === 0) return null
|
|
996
|
+
let summary = sentences[0]
|
|
997
|
+
if (sentences.length > 1 && summary.length + sentences[1].length <= 148) {
|
|
998
|
+
summary += '。' + sentences[1]
|
|
999
|
+
}
|
|
1000
|
+
return summary.length <= 150 ? summary : summary.slice(0, 147) + '...'
|
|
1001
|
+
}
|
|
1002
|
+
|
|
842
1003
|
/** 获取数据库连接(供 FactRetriever 直接使用) */
|
|
843
1004
|
get connection(): Database.Database {
|
|
844
1005
|
return this.db
|
package/src/types.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface RetrieverOptions {
|
|
|
55
55
|
|
|
56
56
|
/** fact_store 工具调用参数 */
|
|
57
57
|
export interface FactStoreArgs {
|
|
58
|
-
action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list' | 'learn' | 'audit'
|
|
58
|
+
action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list' | 'learn' | 'audit' | 'dream'
|
|
59
59
|
content?: string | string[]
|
|
60
60
|
query?: string
|
|
61
61
|
entity?: string
|
|
@@ -82,3 +82,27 @@ export interface SecurityScanResult {
|
|
|
82
82
|
hasPii: boolean
|
|
83
83
|
injectionAttempts: string[]
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/** Dream 整理报告 */
|
|
87
|
+
export interface DreamReport {
|
|
88
|
+
merged: number
|
|
89
|
+
compressed: number
|
|
90
|
+
reclassified: number
|
|
91
|
+
deleted: number
|
|
92
|
+
mergeDetails: Array<{ kept: number; removed: number; similarity: number }>
|
|
93
|
+
health: {
|
|
94
|
+
total: number
|
|
95
|
+
avg_trust: number
|
|
96
|
+
avg_length: number
|
|
97
|
+
coverage: Record<FactCategory, number>
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 精简搜索结果 */
|
|
102
|
+
export interface CompactFactResult {
|
|
103
|
+
factId: number
|
|
104
|
+
display: string
|
|
105
|
+
category: FactCategory
|
|
106
|
+
trustScore: number
|
|
107
|
+
score: number
|
|
108
|
+
}
|
package/tests/resource.test.ts
CHANGED
|
@@ -21,42 +21,44 @@ afterEach(() => {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
describe('ResourceManager', () => {
|
|
24
|
-
it('returns empty
|
|
25
|
-
const
|
|
26
|
-
expect(
|
|
24
|
+
it('returns empty markdown for empty category', () => {
|
|
25
|
+
const text = readResource('identity')
|
|
26
|
+
expect(text).toContain('身份与行为设定')
|
|
27
|
+
expect(text).not.toContain('## 你的身份')
|
|
27
28
|
})
|
|
28
29
|
|
|
29
|
-
it('
|
|
30
|
+
it('formats identity facts as instructions', () => {
|
|
31
|
+
store.addFact('AI角色设定:大名暖暖,身份是用户的编程助手', 'identity')
|
|
32
|
+
const text = readResource('identity')
|
|
33
|
+
expect(text).toContain('你的身份')
|
|
34
|
+
expect(text).toContain('暖暖')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('formats non-identity facts as reference', () => {
|
|
30
38
|
store.addFact('用户偏好深色主题', 'tool_pref')
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(
|
|
34
|
-
// listFacts returns trust_score DESC, first fact added gets default trust
|
|
35
|
-
expect(facts.length).toBeGreaterThan(0)
|
|
39
|
+
const text = readResource('tool_pref')
|
|
40
|
+
expect(text).toContain('工具偏好')
|
|
41
|
+
expect(text).toContain('深色主题')
|
|
36
42
|
})
|
|
37
43
|
|
|
38
44
|
it('caches results', () => {
|
|
39
45
|
store.addFact('测试事实', 'general')
|
|
40
|
-
|
|
46
|
+
readResource('general')
|
|
47
|
+
expect(manager.cacheSize()).toBe(1)
|
|
48
|
+
readResource('general')
|
|
41
49
|
expect(manager.cacheSize()).toBe(1)
|
|
42
|
-
// Second call should hit cache
|
|
43
|
-
const facts2 = manager.getFacts('general')
|
|
44
|
-
expect(facts2.length).toBe(1)
|
|
45
50
|
})
|
|
46
51
|
|
|
47
|
-
it('invalidates cache
|
|
52
|
+
it('invalidates cache', () => {
|
|
48
53
|
store.addFact('测试事实', 'general')
|
|
49
|
-
|
|
54
|
+
readResource('general')
|
|
50
55
|
expect(manager.cacheSize()).toBe(1)
|
|
51
56
|
manager.invalidate()
|
|
52
57
|
expect(manager.cacheSize()).toBe(0)
|
|
53
58
|
})
|
|
54
|
-
|
|
55
|
-
it('limits to top 10 facts', () => {
|
|
56
|
-
for (let i = 0; i < 15; i++) {
|
|
57
|
-
store.addFact(`事实 ${i}`, 'general')
|
|
58
|
-
}
|
|
59
|
-
const facts = manager.getFacts('general')
|
|
60
|
-
expect(facts.length).toBe(10)
|
|
61
|
-
})
|
|
62
59
|
})
|
|
60
|
+
|
|
61
|
+
function readResource(category: string): string {
|
|
62
|
+
const result = (manager as any).readCategory(category)
|
|
63
|
+
return result.contents[0].text
|
|
64
|
+
}
|
package/tests/store.test.ts
CHANGED
|
@@ -153,15 +153,31 @@ describe('logRetrieval', () => {
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
describe('runLearning', () => {
|
|
156
|
-
it('demotes
|
|
156
|
+
it('demotes moderate retrieval low helpful facts', () => {
|
|
157
157
|
const id = store.addFact('demote me', 'general')
|
|
158
|
-
store.connection.prepare('UPDATE facts SET retrieval_count =
|
|
158
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 50, helpful_count = 1, trust_score = 1.0 WHERE fact_id = ?').run(id)
|
|
159
159
|
const result = store.runLearning()
|
|
160
160
|
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
161
161
|
expect(row.trust_score).toBeLessThan(1.0)
|
|
162
162
|
expect(result.demoted).toBeGreaterThanOrEqual(1)
|
|
163
163
|
})
|
|
164
164
|
|
|
165
|
+
it('protects high frequency facts from demotion', () => {
|
|
166
|
+
const id = store.addFact('高频角色设定', 'identity')
|
|
167
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 200, helpful_count = 2, trust_score = 0.9 WHERE fact_id = ?').run(id)
|
|
168
|
+
store.runLearning()
|
|
169
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
170
|
+
expect(row.trust_score).toBe(0.9)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('still demotes moderate frequency facts with low trust', () => {
|
|
174
|
+
const id = store.addFact('中频低信任', 'general')
|
|
175
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 50, helpful_count = 0, trust_score = 0.3 WHERE fact_id = ?').run(id)
|
|
176
|
+
store.runLearning()
|
|
177
|
+
const row = store.connection.prepare('SELECT trust_score FROM facts WHERE fact_id = ?').get(id) as any
|
|
178
|
+
expect(row.trust_score).toBeLessThan(0.3)
|
|
179
|
+
})
|
|
180
|
+
|
|
165
181
|
it('promotes high helpful rate facts', () => {
|
|
166
182
|
const id = store.addFact('promote me', 'general')
|
|
167
183
|
store.connection.prepare('UPDATE facts SET retrieval_count = 50, helpful_count = 20, trust_score = 0.5 WHERE fact_id = ?').run(id)
|
|
@@ -214,3 +230,114 @@ describe('runAudit', () => {
|
|
|
214
230
|
expect(report.long_without_summary.length).toBeGreaterThanOrEqual(1)
|
|
215
231
|
})
|
|
216
232
|
})
|
|
233
|
+
|
|
234
|
+
describe('dream - backup', () => {
|
|
235
|
+
it('creates backup before dream', async () => {
|
|
236
|
+
store.addFact('test fact for backup', 'general')
|
|
237
|
+
const result = await store.backupDatabase()
|
|
238
|
+
expect(result).toBeTruthy()
|
|
239
|
+
expect(result).toContain('dream-')
|
|
240
|
+
expect(result).toContain('.db')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('dream - compress', () => {
|
|
245
|
+
it('generates summary for long facts without summary', () => {
|
|
246
|
+
const longContent = '用户偏好使用 TypeScript 开发前端项目。偏好 React 框架进行组件化开发。' + '额外补充说明'.repeat(50)
|
|
247
|
+
store.addFact(longContent, 'coding_style')
|
|
248
|
+
const result = store.compressLongFacts()
|
|
249
|
+
expect(result).toBeGreaterThanOrEqual(1)
|
|
250
|
+
const row = store.connection.prepare('SELECT summary FROM facts WHERE content = ?').get(longContent) as any
|
|
251
|
+
expect(row.summary).toBeTruthy()
|
|
252
|
+
expect(row.summary.length).toBeLessThanOrEqual(150)
|
|
253
|
+
expect(row.summary).toContain('TypeScript')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('skips facts with existing summary', () => {
|
|
257
|
+
const longContent = 'x'.repeat(300)
|
|
258
|
+
const id = store.addFact(longContent, 'general')
|
|
259
|
+
store.connection.prepare('UPDATE facts SET summary = ? WHERE fact_id = ?').run('existing summary', id)
|
|
260
|
+
const result = store.compressLongFacts()
|
|
261
|
+
expect(result).toBe(0)
|
|
262
|
+
const row = store.connection.prepare('SELECT summary FROM facts WHERE fact_id = ?').get(id) as any
|
|
263
|
+
expect(row.summary).toBe('existing summary')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('skips short facts', () => {
|
|
267
|
+
store.addFact('short fact', 'general')
|
|
268
|
+
const result = store.compressLongFacts()
|
|
269
|
+
expect(result).toBe(0)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('dream - merge', () => {
|
|
274
|
+
it('merges overlapping facts in same category', () => {
|
|
275
|
+
store.addFact('用户偏好使用 TypeScript 编写前端代码', 'coding_style')
|
|
276
|
+
store.addFact('用户偏好使用 TypeScript 编写后端代码', 'coding_style')
|
|
277
|
+
const result = store.mergeOverlappingFacts()
|
|
278
|
+
expect(result.merged).toBeGreaterThanOrEqual(1)
|
|
279
|
+
expect(result.details.length).toBeGreaterThanOrEqual(1)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('protects high frequency facts from deletion', () => {
|
|
283
|
+
const id1 = store.addFact('用户偏好使用 TypeScript 编写前端代码', 'coding_style')
|
|
284
|
+
const id2 = store.addFact('用户偏好使用 TypeScript 编写前端代码扩展', 'coding_style')
|
|
285
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 200 WHERE fact_id = ?').run(id1)
|
|
286
|
+
const result = store.mergeOverlappingFacts()
|
|
287
|
+
const kept = store.connection.prepare('SELECT fact_id FROM facts WHERE fact_id = ?').get(id1) as any
|
|
288
|
+
expect(kept).toBeTruthy()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('does not merge facts across categories', () => {
|
|
292
|
+
store.addFact('用户偏好使用 TypeScript 编写前端代码', 'coding_style')
|
|
293
|
+
store.addFact('用户偏好使用 TypeScript 编写前端代码', 'general')
|
|
294
|
+
const result = store.mergeOverlappingFacts()
|
|
295
|
+
expect(result.merged).toBe(0)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('does not merge when both facts are high frequency', () => {
|
|
299
|
+
const id1 = store.addFact('高频测试事实 AAA 长内容匹配', 'coding_style')
|
|
300
|
+
const id2 = store.addFact('高频测试事实 BBB 长内容匹配', 'coding_style')
|
|
301
|
+
store.connection.prepare('UPDATE facts SET retrieval_count = 200 WHERE fact_id IN (?, ?)').run(id1, id2)
|
|
302
|
+
const result = store.mergeOverlappingFacts()
|
|
303
|
+
// Both facts should still exist
|
|
304
|
+
const row1 = store.connection.prepare('SELECT fact_id FROM facts WHERE fact_id = ?').get(id1) as any
|
|
305
|
+
const row2 = store.connection.prepare('SELECT fact_id FROM facts WHERE fact_id = ?').get(id2) as any
|
|
306
|
+
expect(row1).toBeTruthy()
|
|
307
|
+
expect(row2).toBeTruthy()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('dream - reclassify', () => {
|
|
312
|
+
it('moves miscategorized facts by keywords', () => {
|
|
313
|
+
const id = store.addFact('编码规范:文件不超过 500 行', 'identity')
|
|
314
|
+
const result = store.reclassifyFacts()
|
|
315
|
+
expect(result).toBeGreaterThanOrEqual(1)
|
|
316
|
+
const row = store.connection.prepare('SELECT category FROM facts WHERE fact_id = ?').get(id) as any
|
|
317
|
+
expect(row.category).toBe('coding_style')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('skips correctly categorized facts', () => {
|
|
321
|
+
store.addFact('用户偏好使用 VS Code 编辑器', 'tool_pref')
|
|
322
|
+
const result = store.reclassifyFacts()
|
|
323
|
+
expect(result).toBe(0)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('dream - runDream', () => {
|
|
328
|
+
it('runs full dream cycle and returns report', async () => {
|
|
329
|
+
// 长文 fact(触发压缩)
|
|
330
|
+
store.addFact('用户偏好使用 TypeScript 开发前端。使用 React 框架。' + 'x'.repeat(250), 'coding_style')
|
|
331
|
+
// 重叠 fact(触发合并)
|
|
332
|
+
store.addFact('用户偏好使用 TypeScript 开发前端代码', 'coding_style')
|
|
333
|
+
// 分类错误 fact(触发重分类)
|
|
334
|
+
store.addFact('编码规范:文件不超过 500 行', 'identity')
|
|
335
|
+
|
|
336
|
+
const report = await store.runDream({ skipBackup: true })
|
|
337
|
+
expect(report.compressed).toBeGreaterThanOrEqual(0)
|
|
338
|
+
expect(report.merged).toBeGreaterThanOrEqual(0)
|
|
339
|
+
expect(report.reclassified).toBeGreaterThanOrEqual(0)
|
|
340
|
+
expect(report.health.total).toBeGreaterThanOrEqual(1)
|
|
341
|
+
expect(report.health.coverage).toBeTruthy()
|
|
342
|
+
})
|
|
343
|
+
})
|