@timmeck/brain-core 2.34.1 → 2.35.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/concept-abstraction/concept-abstraction.d.ts +139 -0
- package/dist/concept-abstraction/concept-abstraction.js +534 -0
- package/dist/concept-abstraction/concept-abstraction.js.map +1 -0
- package/dist/concept-abstraction/index.d.ts +2 -0
- package/dist/concept-abstraction/index.js +2 -0
- package/dist/concept-abstraction/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-palace/memory-palace.d.ts +2 -2
- package/dist/memory-palace/memory-palace.js.map +1 -1
- package/dist/research/bootstrap-service.js +48 -34
- package/dist/research/bootstrap-service.js.map +1 -1
- package/dist/research/research-orchestrator.d.ts +5 -0
- package/dist/research/research-orchestrator.js +201 -4
- package/dist/research/research-orchestrator.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { ThoughtStream } from '../consciousness/thought-stream.js';
|
|
3
|
+
import type { MemoryPalace } from '../memory-palace/memory-palace.js';
|
|
4
|
+
export interface ConceptAbstractionConfig {
|
|
5
|
+
brainName: string;
|
|
6
|
+
/** Minimum Dice similarity to form a cluster. Default: 0.35 */
|
|
7
|
+
clusterThreshold?: number;
|
|
8
|
+
/** Minimum cluster size to keep. Default: 3 */
|
|
9
|
+
minClusterSize?: number;
|
|
10
|
+
/** Level-1 re-cluster threshold. Default: 0.25 */
|
|
11
|
+
level1Threshold?: number;
|
|
12
|
+
/** Level-2 re-cluster threshold. Default: 0.20 */
|
|
13
|
+
level2Threshold?: number;
|
|
14
|
+
/** Minimum word occurrence ratio for keyword extraction. Default: 0.6 */
|
|
15
|
+
keywordMinRatio?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ConceptDataSources {
|
|
18
|
+
getPrinciples: (domain?: string, limit?: number) => Array<{
|
|
19
|
+
id?: string | number;
|
|
20
|
+
statement: string;
|
|
21
|
+
confidence: number;
|
|
22
|
+
domain?: string;
|
|
23
|
+
}>;
|
|
24
|
+
getAntiPatterns: (domain?: string, limit?: number) => Array<{
|
|
25
|
+
id?: string | number;
|
|
26
|
+
statement: string;
|
|
27
|
+
confidence?: number;
|
|
28
|
+
domain?: string;
|
|
29
|
+
}>;
|
|
30
|
+
getHypotheses?: (status?: string, limit?: number) => Array<{
|
|
31
|
+
id?: string | number;
|
|
32
|
+
statement: string;
|
|
33
|
+
confidence?: number;
|
|
34
|
+
domain?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface AbstractConcept {
|
|
38
|
+
id?: number;
|
|
39
|
+
title: string;
|
|
40
|
+
description: string;
|
|
41
|
+
level: number;
|
|
42
|
+
parentId: number | null;
|
|
43
|
+
domain: string;
|
|
44
|
+
memberCount: number;
|
|
45
|
+
avgConfidence: number;
|
|
46
|
+
avgSimilarity: number;
|
|
47
|
+
keywords: string[];
|
|
48
|
+
transferability: number;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ConceptMember {
|
|
53
|
+
conceptId: number;
|
|
54
|
+
memberType: MemberType;
|
|
55
|
+
memberId: number;
|
|
56
|
+
similarityToCentroid: number;
|
|
57
|
+
}
|
|
58
|
+
export type MemberType = 'principle' | 'anti_pattern' | 'strategy' | 'hypothesis' | 'concept';
|
|
59
|
+
export interface ConceptHistoryEntry {
|
|
60
|
+
cycle: number;
|
|
61
|
+
totalConcepts: number;
|
|
62
|
+
conceptsByLevel: Record<number, number>;
|
|
63
|
+
newCount: number;
|
|
64
|
+
mergedCount: number;
|
|
65
|
+
avgTransferability: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ConceptHierarchy {
|
|
68
|
+
concept: AbstractConcept;
|
|
69
|
+
children: ConceptHierarchy[];
|
|
70
|
+
members: ConceptMember[];
|
|
71
|
+
}
|
|
72
|
+
export interface ConceptStatus {
|
|
73
|
+
totalConcepts: number;
|
|
74
|
+
conceptsByLevel: Record<number, number>;
|
|
75
|
+
avgTransferability: number;
|
|
76
|
+
topConcepts: AbstractConcept[];
|
|
77
|
+
recentHistory: ConceptHistoryEntry[];
|
|
78
|
+
cycleCount: number;
|
|
79
|
+
}
|
|
80
|
+
export declare function runConceptAbstractionMigration(db: Database.Database): void;
|
|
81
|
+
export declare class ConceptAbstraction {
|
|
82
|
+
private readonly db;
|
|
83
|
+
private readonly config;
|
|
84
|
+
private readonly log;
|
|
85
|
+
private ts;
|
|
86
|
+
private sources;
|
|
87
|
+
private cycleCount;
|
|
88
|
+
private readonly insertConcept;
|
|
89
|
+
private readonly insertMember;
|
|
90
|
+
private readonly insertHistory;
|
|
91
|
+
private readonly getConceptById;
|
|
92
|
+
private readonly getMembersByConcept;
|
|
93
|
+
private readonly getConceptsByLevelStmt;
|
|
94
|
+
private readonly getChildConcepts;
|
|
95
|
+
private readonly getTopConceptsStmt;
|
|
96
|
+
private readonly getTransferableStmt;
|
|
97
|
+
private readonly getHistoryStmt;
|
|
98
|
+
private readonly countConceptsStmt;
|
|
99
|
+
private readonly countByLevelStmt;
|
|
100
|
+
private readonly avgTransferabilityStmt;
|
|
101
|
+
private readonly clearConceptsStmt;
|
|
102
|
+
private readonly clearMembersStmt;
|
|
103
|
+
constructor(db: Database.Database, config: ConceptAbstractionConfig);
|
|
104
|
+
setThoughtStream(stream: ThoughtStream): void;
|
|
105
|
+
setDataSources(sources: ConceptDataSources): void;
|
|
106
|
+
/** Run concept formation: gather → cluster → hierarchy → persist */
|
|
107
|
+
formConcepts(): {
|
|
108
|
+
newConcepts: number;
|
|
109
|
+
totalConcepts: number;
|
|
110
|
+
levels: Record<number, number>;
|
|
111
|
+
};
|
|
112
|
+
getConceptsByLevel(level: number): AbstractConcept[];
|
|
113
|
+
getHierarchy(conceptId: number): ConceptHierarchy | null;
|
|
114
|
+
getMembers(conceptId: number): ConceptMember[];
|
|
115
|
+
getTransferableConcepts(minTransferability?: number): AbstractConcept[];
|
|
116
|
+
/** Register all concepts as nodes in MemoryPalace and link members. */
|
|
117
|
+
registerInPalace(palace: MemoryPalace): number;
|
|
118
|
+
getStatus(): ConceptStatus;
|
|
119
|
+
private gatherItems;
|
|
120
|
+
/** Greedy agglomerative clustering via bigram Dice similarity. */
|
|
121
|
+
private clusterItems;
|
|
122
|
+
/** Persist clusters as AbstractConcepts with members. Returns array of concept IDs. */
|
|
123
|
+
private persistClusters;
|
|
124
|
+
/** Convert persisted concepts to KnowledgeItems for re-clustering. */
|
|
125
|
+
private conceptsToItems;
|
|
126
|
+
private findConceptByTitle;
|
|
127
|
+
/** Bigram Dice coefficient — same algorithm as MemoryPalace and DreamConsolidator. */
|
|
128
|
+
private textOverlap;
|
|
129
|
+
private bigrams;
|
|
130
|
+
private generateTitle;
|
|
131
|
+
private generateDescription;
|
|
132
|
+
private extractKeywords;
|
|
133
|
+
private avgClusterSimilarity;
|
|
134
|
+
private computeTransferability;
|
|
135
|
+
private computeDomain;
|
|
136
|
+
private mostCommon;
|
|
137
|
+
private getTotalCount;
|
|
138
|
+
private getLevelCounts;
|
|
139
|
+
}
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
|
+
// ── Migration ───────────────────────────────────────────
|
|
3
|
+
export function runConceptAbstractionMigration(db) {
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS abstract_concepts (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
title TEXT NOT NULL,
|
|
8
|
+
description TEXT NOT NULL DEFAULT '',
|
|
9
|
+
level INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
parent_id INTEGER,
|
|
11
|
+
domain TEXT NOT NULL DEFAULT 'general',
|
|
12
|
+
member_count INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
avg_confidence REAL NOT NULL DEFAULT 0,
|
|
14
|
+
avg_similarity REAL NOT NULL DEFAULT 0,
|
|
15
|
+
keywords TEXT NOT NULL DEFAULT '[]',
|
|
16
|
+
transferability REAL NOT NULL DEFAULT 0,
|
|
17
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
19
|
+
FOREIGN KEY (parent_id) REFERENCES abstract_concepts(id) ON DELETE SET NULL
|
|
20
|
+
);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_abstract_concepts_level ON abstract_concepts(level);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_abstract_concepts_parent ON abstract_concepts(parent_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_abstract_concepts_transferability ON abstract_concepts(transferability DESC);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS concept_members (
|
|
26
|
+
concept_id INTEGER NOT NULL,
|
|
27
|
+
member_type TEXT NOT NULL,
|
|
28
|
+
member_id INTEGER NOT NULL,
|
|
29
|
+
similarity_to_centroid REAL NOT NULL DEFAULT 0,
|
|
30
|
+
PRIMARY KEY (concept_id, member_type, member_id),
|
|
31
|
+
FOREIGN KEY (concept_id) REFERENCES abstract_concepts(id) ON DELETE CASCADE
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_concept_members_type ON concept_members(member_type);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS concept_history (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
cycle INTEGER NOT NULL,
|
|
38
|
+
total_concepts INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
concepts_by_level TEXT NOT NULL DEFAULT '{}',
|
|
40
|
+
new_count INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
merged_count INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
avg_transferability REAL NOT NULL DEFAULT 0,
|
|
43
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
44
|
+
);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_concept_history_cycle ON concept_history(cycle DESC);
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
// ── Engine ──────────────────────────────────────────────
|
|
49
|
+
export class ConceptAbstraction {
|
|
50
|
+
db;
|
|
51
|
+
config;
|
|
52
|
+
log = getLogger();
|
|
53
|
+
ts = null;
|
|
54
|
+
sources = null;
|
|
55
|
+
cycleCount = 0;
|
|
56
|
+
// Prepared statements
|
|
57
|
+
insertConcept;
|
|
58
|
+
insertMember;
|
|
59
|
+
insertHistory;
|
|
60
|
+
getConceptById;
|
|
61
|
+
getMembersByConcept;
|
|
62
|
+
getConceptsByLevelStmt;
|
|
63
|
+
getChildConcepts;
|
|
64
|
+
getTopConceptsStmt;
|
|
65
|
+
getTransferableStmt;
|
|
66
|
+
getHistoryStmt;
|
|
67
|
+
countConceptsStmt;
|
|
68
|
+
countByLevelStmt;
|
|
69
|
+
avgTransferabilityStmt;
|
|
70
|
+
clearConceptsStmt;
|
|
71
|
+
clearMembersStmt;
|
|
72
|
+
constructor(db, config) {
|
|
73
|
+
this.db = db;
|
|
74
|
+
this.config = {
|
|
75
|
+
brainName: config.brainName,
|
|
76
|
+
clusterThreshold: config.clusterThreshold ?? 0.35,
|
|
77
|
+
minClusterSize: config.minClusterSize ?? 3,
|
|
78
|
+
level1Threshold: config.level1Threshold ?? 0.25,
|
|
79
|
+
level2Threshold: config.level2Threshold ?? 0.20,
|
|
80
|
+
keywordMinRatio: config.keywordMinRatio ?? 0.6,
|
|
81
|
+
};
|
|
82
|
+
runConceptAbstractionMigration(db);
|
|
83
|
+
this.insertConcept = db.prepare(`
|
|
84
|
+
INSERT INTO abstract_concepts (title, description, level, parent_id, domain, member_count, avg_confidence, avg_similarity, keywords, transferability)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
`);
|
|
87
|
+
this.insertMember = db.prepare(`
|
|
88
|
+
INSERT OR REPLACE INTO concept_members (concept_id, member_type, member_id, similarity_to_centroid)
|
|
89
|
+
VALUES (?, ?, ?, ?)
|
|
90
|
+
`);
|
|
91
|
+
this.insertHistory = db.prepare(`
|
|
92
|
+
INSERT INTO concept_history (cycle, total_concepts, concepts_by_level, new_count, merged_count, avg_transferability)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
94
|
+
`);
|
|
95
|
+
this.getConceptById = db.prepare('SELECT * FROM abstract_concepts WHERE id = ?');
|
|
96
|
+
this.getMembersByConcept = db.prepare('SELECT * FROM concept_members WHERE concept_id = ?');
|
|
97
|
+
this.getConceptsByLevelStmt = db.prepare('SELECT * FROM abstract_concepts WHERE level = ? ORDER BY member_count DESC');
|
|
98
|
+
this.getChildConcepts = db.prepare('SELECT * FROM abstract_concepts WHERE parent_id = ? ORDER BY member_count DESC');
|
|
99
|
+
this.getTopConceptsStmt = db.prepare('SELECT * FROM abstract_concepts ORDER BY member_count DESC, transferability DESC LIMIT ?');
|
|
100
|
+
this.getTransferableStmt = db.prepare('SELECT * FROM abstract_concepts WHERE transferability >= ? ORDER BY transferability DESC, member_count DESC');
|
|
101
|
+
this.getHistoryStmt = db.prepare('SELECT * FROM concept_history ORDER BY cycle DESC LIMIT ?');
|
|
102
|
+
this.countConceptsStmt = db.prepare('SELECT COUNT(*) as count FROM abstract_concepts');
|
|
103
|
+
this.countByLevelStmt = db.prepare('SELECT level, COUNT(*) as count FROM abstract_concepts GROUP BY level');
|
|
104
|
+
this.avgTransferabilityStmt = db.prepare('SELECT AVG(transferability) as avg FROM abstract_concepts');
|
|
105
|
+
this.clearConceptsStmt = db.prepare('DELETE FROM abstract_concepts');
|
|
106
|
+
this.clearMembersStmt = db.prepare('DELETE FROM concept_members');
|
|
107
|
+
}
|
|
108
|
+
setThoughtStream(stream) {
|
|
109
|
+
this.ts = stream;
|
|
110
|
+
}
|
|
111
|
+
setDataSources(sources) {
|
|
112
|
+
this.sources = sources;
|
|
113
|
+
}
|
|
114
|
+
// ── Main algorithm ────────────────────────────────────
|
|
115
|
+
/** Run concept formation: gather → cluster → hierarchy → persist */
|
|
116
|
+
formConcepts() {
|
|
117
|
+
this.cycleCount++;
|
|
118
|
+
const ts = this.ts;
|
|
119
|
+
ts?.emit('concept_abstraction', 'analyzing', 'Forming abstract concepts from knowledge...', 'routine');
|
|
120
|
+
if (!this.sources) {
|
|
121
|
+
this.log.warn('[concept-abstraction] No data sources set');
|
|
122
|
+
return { newConcepts: 0, totalConcepts: 0, levels: {} };
|
|
123
|
+
}
|
|
124
|
+
// Phase 1: Gather all knowledge items
|
|
125
|
+
const items = this.gatherItems();
|
|
126
|
+
if (items.length < this.config.minClusterSize) {
|
|
127
|
+
ts?.emit('concept_abstraction', 'reflecting', `Not enough items to cluster (${items.length})`, 'routine');
|
|
128
|
+
return { newConcepts: 0, totalConcepts: this.getTotalCount(), levels: this.getLevelCounts() };
|
|
129
|
+
}
|
|
130
|
+
// Count existing before clearing
|
|
131
|
+
const existingCount = this.getTotalCount();
|
|
132
|
+
// Clear old concepts for full rebuild
|
|
133
|
+
this.db.transaction(() => {
|
|
134
|
+
this.clearMembersStmt.run();
|
|
135
|
+
this.clearConceptsStmt.run();
|
|
136
|
+
})();
|
|
137
|
+
// Phase 2: Cluster level 0 (concrete → abstract)
|
|
138
|
+
const level0Clusters = this.clusterItems(items, this.config.clusterThreshold);
|
|
139
|
+
const level0Concepts = this.persistClusters(level0Clusters, 0, null);
|
|
140
|
+
// Phase 3: Hierarchy — re-cluster level 0 concepts into level 1
|
|
141
|
+
let level1Concepts = [];
|
|
142
|
+
if (level0Concepts.length >= this.config.minClusterSize) {
|
|
143
|
+
const l0Items = this.conceptsToItems(level0Concepts);
|
|
144
|
+
const level1Clusters = this.clusterItems(l0Items, this.config.level1Threshold);
|
|
145
|
+
level1Concepts = this.persistClusters(level1Clusters, 1, null);
|
|
146
|
+
// Link level-0 → level-1 parentage
|
|
147
|
+
for (const cluster of level1Clusters) {
|
|
148
|
+
const parentId = this.findConceptByTitle(cluster.centroid.text, 1);
|
|
149
|
+
if (parentId) {
|
|
150
|
+
for (const item of cluster.items) {
|
|
151
|
+
if (item.type === 'concept') {
|
|
152
|
+
this.db.prepare('UPDATE abstract_concepts SET parent_id = ? WHERE id = ?').run(parentId, item.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Phase 3b: level 1 → level 2
|
|
159
|
+
let level2Concepts = [];
|
|
160
|
+
if (level1Concepts.length >= this.config.minClusterSize) {
|
|
161
|
+
const l1Items = this.conceptsToItems(level1Concepts);
|
|
162
|
+
const level2Clusters = this.clusterItems(l1Items, this.config.level2Threshold);
|
|
163
|
+
level2Concepts = this.persistClusters(level2Clusters, 2, null);
|
|
164
|
+
for (const cluster of level2Clusters) {
|
|
165
|
+
const parentId = this.findConceptByTitle(cluster.centroid.text, 2);
|
|
166
|
+
if (parentId) {
|
|
167
|
+
for (const item of cluster.items) {
|
|
168
|
+
if (item.type === 'concept') {
|
|
169
|
+
this.db.prepare('UPDATE abstract_concepts SET parent_id = ? WHERE id = ?').run(parentId, item.id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const totalConcepts = this.getTotalCount();
|
|
176
|
+
const levels = this.getLevelCounts();
|
|
177
|
+
const newCount = Math.max(0, totalConcepts - existingCount);
|
|
178
|
+
const mergedCount = Math.max(0, existingCount - totalConcepts);
|
|
179
|
+
const avgTransfer = this.avgTransferabilityStmt.get()?.avg ?? 0;
|
|
180
|
+
// Record history
|
|
181
|
+
this.insertHistory.run(this.cycleCount, totalConcepts, JSON.stringify(levels), newCount, mergedCount, avgTransfer);
|
|
182
|
+
ts?.emit('concept_abstraction', 'discovering', `Formed ${totalConcepts} concepts (L0: ${levels[0] ?? 0}, L1: ${levels[1] ?? 0}, L2: ${levels[2] ?? 0})`, totalConcepts > 0 ? 'notable' : 'routine');
|
|
183
|
+
this.log.info(`[concept-abstraction] Formed ${totalConcepts} concepts across ${Object.keys(levels).length} levels`);
|
|
184
|
+
return { newConcepts: newCount, totalConcepts, levels };
|
|
185
|
+
}
|
|
186
|
+
// ── Queries ───────────────────────────────────────────
|
|
187
|
+
getConceptsByLevel(level) {
|
|
188
|
+
const rows = this.getConceptsByLevelStmt.all(level);
|
|
189
|
+
return rows.map(toAbstractConcept);
|
|
190
|
+
}
|
|
191
|
+
getHierarchy(conceptId) {
|
|
192
|
+
const row = this.getConceptById.get(conceptId);
|
|
193
|
+
if (!row)
|
|
194
|
+
return null;
|
|
195
|
+
const concept = toAbstractConcept(row);
|
|
196
|
+
const members = this.getMembersByConcept.all(conceptId).map(toConceptMember);
|
|
197
|
+
const children = this.getChildConcepts.all(conceptId).map(r => {
|
|
198
|
+
const child = toAbstractConcept(r);
|
|
199
|
+
const childMembers = this.getMembersByConcept.all(r.id).map(toConceptMember);
|
|
200
|
+
const grandchildren = this.getChildConcepts.all(r.id).map(gc => ({
|
|
201
|
+
concept: toAbstractConcept(gc),
|
|
202
|
+
children: [],
|
|
203
|
+
members: this.getMembersByConcept.all(gc.id).map(toConceptMember),
|
|
204
|
+
}));
|
|
205
|
+
return { concept: child, children: grandchildren, members: childMembers };
|
|
206
|
+
});
|
|
207
|
+
return { concept, children, members };
|
|
208
|
+
}
|
|
209
|
+
getMembers(conceptId) {
|
|
210
|
+
return this.getMembersByConcept.all(conceptId).map(toConceptMember);
|
|
211
|
+
}
|
|
212
|
+
getTransferableConcepts(minTransferability = 0.3) {
|
|
213
|
+
return this.getTransferableStmt.all(minTransferability).map(toAbstractConcept);
|
|
214
|
+
}
|
|
215
|
+
/** Register all concepts as nodes in MemoryPalace and link members. */
|
|
216
|
+
registerInPalace(palace) {
|
|
217
|
+
const concepts = this.getTopConceptsStmt.all(500).map(toAbstractConcept);
|
|
218
|
+
let registered = 0;
|
|
219
|
+
for (const concept of concepts) {
|
|
220
|
+
// Register concept node
|
|
221
|
+
palace.addConnection('concept', String(concept.id), 'concept', String(concept.id), 'related_to', 0);
|
|
222
|
+
// Link members → concept
|
|
223
|
+
const members = this.getMembers(concept.id);
|
|
224
|
+
for (const member of members) {
|
|
225
|
+
if (member.memberType !== 'concept') {
|
|
226
|
+
palace.addConnection(member.memberType, String(member.memberId), 'concept', String(concept.id), 'abstracted_from', member.similarityToCentroid);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Link child concepts with 'generalizes'
|
|
230
|
+
if (concept.parentId) {
|
|
231
|
+
palace.addConnection('concept', String(concept.parentId), 'concept', String(concept.id), 'generalizes', concept.avgSimilarity);
|
|
232
|
+
}
|
|
233
|
+
registered++;
|
|
234
|
+
}
|
|
235
|
+
return registered;
|
|
236
|
+
}
|
|
237
|
+
getStatus() {
|
|
238
|
+
return {
|
|
239
|
+
totalConcepts: this.getTotalCount(),
|
|
240
|
+
conceptsByLevel: this.getLevelCounts(),
|
|
241
|
+
avgTransferability: this.avgTransferabilityStmt.get()?.avg ?? 0,
|
|
242
|
+
topConcepts: this.getTopConceptsStmt.all(10).map(toAbstractConcept),
|
|
243
|
+
recentHistory: this.getHistoryStmt.all(10).map(toHistoryEntry),
|
|
244
|
+
cycleCount: this.cycleCount,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// ── Private: Gathering ────────────────────────────────
|
|
248
|
+
gatherItems() {
|
|
249
|
+
const items = [];
|
|
250
|
+
if (!this.sources)
|
|
251
|
+
return items;
|
|
252
|
+
try {
|
|
253
|
+
const principles = this.sources.getPrinciples(undefined, 500);
|
|
254
|
+
for (const p of principles) {
|
|
255
|
+
items.push({
|
|
256
|
+
type: 'principle',
|
|
257
|
+
id: typeof p.id === 'string' ? parseInt(p.id, 10) || 0 : (p.id ?? 0),
|
|
258
|
+
text: p.statement,
|
|
259
|
+
confidence: p.confidence,
|
|
260
|
+
domain: p.domain ?? 'general',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
this.log.warn(`[concept-abstraction] Error gathering principles: ${err.message}`);
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const antiPatterns = this.sources.getAntiPatterns(undefined, 500);
|
|
269
|
+
for (const ap of antiPatterns) {
|
|
270
|
+
items.push({
|
|
271
|
+
type: 'anti_pattern',
|
|
272
|
+
id: typeof ap.id === 'string' ? parseInt(ap.id, 10) || 0 : (ap.id ?? 0),
|
|
273
|
+
text: ap.statement,
|
|
274
|
+
confidence: ap.confidence ?? 0.7,
|
|
275
|
+
domain: ap.domain ?? 'general',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
this.log.warn(`[concept-abstraction] Error gathering anti-patterns: ${err.message}`);
|
|
281
|
+
}
|
|
282
|
+
if (this.sources.getHypotheses) {
|
|
283
|
+
try {
|
|
284
|
+
const hypotheses = this.sources.getHypotheses('confirmed', 500);
|
|
285
|
+
for (const h of hypotheses) {
|
|
286
|
+
items.push({
|
|
287
|
+
type: 'hypothesis',
|
|
288
|
+
id: typeof h.id === 'string' ? parseInt(h.id, 10) || 0 : (h.id ?? 0),
|
|
289
|
+
text: h.statement,
|
|
290
|
+
confidence: h.confidence ?? 0.5,
|
|
291
|
+
domain: h.domain ?? 'general',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
this.log.warn(`[concept-abstraction] Error gathering hypotheses: ${err.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return items;
|
|
300
|
+
}
|
|
301
|
+
// ── Private: Clustering ───────────────────────────────
|
|
302
|
+
/** Greedy agglomerative clustering via bigram Dice similarity. */
|
|
303
|
+
clusterItems(items, threshold) {
|
|
304
|
+
const assigned = new Set();
|
|
305
|
+
const clusters = [];
|
|
306
|
+
// Sort by confidence descending — best items become centroids
|
|
307
|
+
const sorted = [...items].sort((a, b) => b.confidence - a.confidence);
|
|
308
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
309
|
+
if (assigned.has(i))
|
|
310
|
+
continue;
|
|
311
|
+
const centroid = sorted[i];
|
|
312
|
+
const cluster = [centroid];
|
|
313
|
+
assigned.add(i);
|
|
314
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
315
|
+
if (assigned.has(j))
|
|
316
|
+
continue;
|
|
317
|
+
const sim = this.textOverlap(centroid.text, sorted[j].text);
|
|
318
|
+
if (sim >= threshold) {
|
|
319
|
+
cluster.push(sorted[j]);
|
|
320
|
+
assigned.add(j);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (cluster.length >= this.config.minClusterSize) {
|
|
324
|
+
clusters.push({ items: cluster, centroid });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return clusters;
|
|
328
|
+
}
|
|
329
|
+
/** Persist clusters as AbstractConcepts with members. Returns array of concept IDs. */
|
|
330
|
+
persistClusters(clusters, level, _parentId) {
|
|
331
|
+
const ids = [];
|
|
332
|
+
for (const cluster of clusters) {
|
|
333
|
+
const title = this.generateTitle(cluster);
|
|
334
|
+
const description = this.generateDescription(cluster);
|
|
335
|
+
const keywords = this.extractKeywords(cluster);
|
|
336
|
+
const avgConfidence = cluster.items.reduce((s, it) => s + it.confidence, 0) / cluster.items.length;
|
|
337
|
+
const avgSim = this.avgClusterSimilarity(cluster);
|
|
338
|
+
const transferability = this.computeTransferability(cluster);
|
|
339
|
+
const domain = this.computeDomain(cluster);
|
|
340
|
+
const result = this.insertConcept.run(title, description, level, null, // parent_id set later
|
|
341
|
+
domain, cluster.items.length, avgConfidence, avgSim, JSON.stringify(keywords), transferability);
|
|
342
|
+
const conceptId = Number(result.lastInsertRowid);
|
|
343
|
+
ids.push(conceptId);
|
|
344
|
+
// Insert members
|
|
345
|
+
for (const item of cluster.items) {
|
|
346
|
+
const sim = this.textOverlap(cluster.centroid.text, item.text);
|
|
347
|
+
this.insertMember.run(conceptId, item.type, item.id, sim);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return ids;
|
|
351
|
+
}
|
|
352
|
+
/** Convert persisted concepts to KnowledgeItems for re-clustering. */
|
|
353
|
+
conceptsToItems(conceptIds) {
|
|
354
|
+
return conceptIds.map(id => {
|
|
355
|
+
const row = this.getConceptById.get(id);
|
|
356
|
+
if (!row)
|
|
357
|
+
return null;
|
|
358
|
+
return {
|
|
359
|
+
type: 'concept',
|
|
360
|
+
id: row.id,
|
|
361
|
+
text: `${row.title}: ${row.description}`,
|
|
362
|
+
confidence: row.avg_confidence,
|
|
363
|
+
domain: row.domain,
|
|
364
|
+
};
|
|
365
|
+
}).filter((x) => x !== null);
|
|
366
|
+
}
|
|
367
|
+
findConceptByTitle(text, level) {
|
|
368
|
+
const concepts = this.getConceptsByLevelStmt.all(level);
|
|
369
|
+
// Find best match by title overlap
|
|
370
|
+
let bestId = null;
|
|
371
|
+
let bestSim = 0;
|
|
372
|
+
for (const c of concepts) {
|
|
373
|
+
const candidateText = `${c.title}: ${c.description}`;
|
|
374
|
+
const sim = this.textOverlap(text, candidateText);
|
|
375
|
+
if (sim > bestSim) {
|
|
376
|
+
bestSim = sim;
|
|
377
|
+
bestId = c.id;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return bestSim > 0.15 ? bestId : null;
|
|
381
|
+
}
|
|
382
|
+
// ── Private: Text analysis ────────────────────────────
|
|
383
|
+
/** Bigram Dice coefficient — same algorithm as MemoryPalace and DreamConsolidator. */
|
|
384
|
+
textOverlap(a, b) {
|
|
385
|
+
if (!a || !b)
|
|
386
|
+
return 0;
|
|
387
|
+
const aBigrams = this.bigrams(a.toLowerCase());
|
|
388
|
+
const bBigrams = this.bigrams(b.toLowerCase());
|
|
389
|
+
if (aBigrams.size === 0 || bBigrams.size === 0)
|
|
390
|
+
return 0;
|
|
391
|
+
let intersection = 0;
|
|
392
|
+
for (const bg of aBigrams) {
|
|
393
|
+
if (bBigrams.has(bg))
|
|
394
|
+
intersection++;
|
|
395
|
+
}
|
|
396
|
+
return (2 * intersection) / (aBigrams.size + bBigrams.size);
|
|
397
|
+
}
|
|
398
|
+
bigrams(text) {
|
|
399
|
+
const words = text.split(/\s+/).filter(w => w.length > 2);
|
|
400
|
+
const result = new Set();
|
|
401
|
+
for (const word of words) {
|
|
402
|
+
for (let i = 0; i < word.length - 1; i++) {
|
|
403
|
+
result.add(word.substring(i, i + 2));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
generateTitle(cluster) {
|
|
409
|
+
const keywords = this.extractKeywords(cluster);
|
|
410
|
+
if (keywords.length >= 2)
|
|
411
|
+
return keywords.slice(0, 3).join(' + ');
|
|
412
|
+
// Fallback: use centroid's first few meaningful words
|
|
413
|
+
const words = cluster.centroid.text.split(/\s+/).filter(w => w.length > 3).slice(0, 4);
|
|
414
|
+
return words.join(' ') || 'Abstract Concept';
|
|
415
|
+
}
|
|
416
|
+
generateDescription(cluster) {
|
|
417
|
+
const types = new Set(cluster.items.map(i => i.type));
|
|
418
|
+
const typeStr = [...types].join(', ');
|
|
419
|
+
return `Abstraction of ${cluster.items.length} knowledge items (${typeStr}) centered around: ${cluster.centroid.text.substring(0, 120)}`;
|
|
420
|
+
}
|
|
421
|
+
extractKeywords(cluster) {
|
|
422
|
+
const wordCounts = new Map();
|
|
423
|
+
const stopwords = new Set([
|
|
424
|
+
'the', 'and', 'for', 'that', 'this', 'with', 'from', 'are', 'not', 'was', 'but', 'has', 'have', 'had',
|
|
425
|
+
'been', 'will', 'can', 'may', 'should', 'could', 'would', 'more', 'than', 'also', 'its', 'into',
|
|
426
|
+
'when', 'where', 'which', 'their', 'them', 'then', 'there', 'these', 'those', 'being', 'each',
|
|
427
|
+
'der', 'die', 'das', 'und', 'ist', 'ein', 'eine', 'den', 'dem', 'des', 'sich', 'mit', 'auf',
|
|
428
|
+
'von', 'als', 'für', 'nicht', 'auch', 'nur', 'noch', 'oder', 'aber', 'nach', 'wie', 'bei',
|
|
429
|
+
]);
|
|
430
|
+
for (const item of cluster.items) {
|
|
431
|
+
const words = item.text.toLowerCase().split(/\s+/).filter(w => w.length > 3 && !stopwords.has(w));
|
|
432
|
+
const unique = new Set(words);
|
|
433
|
+
for (const w of unique) {
|
|
434
|
+
wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const minOccurrence = Math.ceil(cluster.items.length * this.config.keywordMinRatio);
|
|
438
|
+
return [...wordCounts.entries()]
|
|
439
|
+
.filter(([, count]) => count >= minOccurrence)
|
|
440
|
+
.sort((a, b) => b[1] - a[1])
|
|
441
|
+
.slice(0, 8)
|
|
442
|
+
.map(([word]) => word);
|
|
443
|
+
}
|
|
444
|
+
avgClusterSimilarity(cluster) {
|
|
445
|
+
if (cluster.items.length <= 1)
|
|
446
|
+
return 1;
|
|
447
|
+
let total = 0;
|
|
448
|
+
let count = 0;
|
|
449
|
+
for (const item of cluster.items) {
|
|
450
|
+
if (item !== cluster.centroid) {
|
|
451
|
+
total += this.textOverlap(cluster.centroid.text, item.text);
|
|
452
|
+
count++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return count > 0 ? total / count : 0;
|
|
456
|
+
}
|
|
457
|
+
computeTransferability(cluster) {
|
|
458
|
+
const domains = new Set(cluster.items.map(i => i.domain));
|
|
459
|
+
if (domains.size <= 1)
|
|
460
|
+
return 0;
|
|
461
|
+
// Cross-domain items / total
|
|
462
|
+
const mainDomain = this.mostCommon(cluster.items.map(i => i.domain));
|
|
463
|
+
const crossDomain = cluster.items.filter(i => i.domain !== mainDomain).length;
|
|
464
|
+
return crossDomain / cluster.items.length;
|
|
465
|
+
}
|
|
466
|
+
computeDomain(cluster) {
|
|
467
|
+
const domains = cluster.items.map(i => i.domain);
|
|
468
|
+
const unique = new Set(domains);
|
|
469
|
+
if (unique.size > 1)
|
|
470
|
+
return 'cross-domain';
|
|
471
|
+
return domains[0] ?? 'general';
|
|
472
|
+
}
|
|
473
|
+
mostCommon(arr) {
|
|
474
|
+
const counts = new Map();
|
|
475
|
+
for (const item of arr)
|
|
476
|
+
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
477
|
+
let best = arr[0] ?? 'general';
|
|
478
|
+
let bestCount = 0;
|
|
479
|
+
for (const [key, count] of counts) {
|
|
480
|
+
if (count > bestCount) {
|
|
481
|
+
best = key;
|
|
482
|
+
bestCount = count;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return best;
|
|
486
|
+
}
|
|
487
|
+
// ── Private: Helpers ──────────────────────────────────
|
|
488
|
+
getTotalCount() {
|
|
489
|
+
return this.countConceptsStmt.get().count;
|
|
490
|
+
}
|
|
491
|
+
getLevelCounts() {
|
|
492
|
+
const rows = this.countByLevelStmt.all();
|
|
493
|
+
const result = {};
|
|
494
|
+
for (const row of rows)
|
|
495
|
+
result[row.level] = row.count;
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function toAbstractConcept(row) {
|
|
500
|
+
return {
|
|
501
|
+
id: row.id,
|
|
502
|
+
title: row.title,
|
|
503
|
+
description: row.description,
|
|
504
|
+
level: row.level,
|
|
505
|
+
parentId: row.parent_id,
|
|
506
|
+
domain: row.domain,
|
|
507
|
+
memberCount: row.member_count,
|
|
508
|
+
avgConfidence: row.avg_confidence,
|
|
509
|
+
avgSimilarity: row.avg_similarity,
|
|
510
|
+
keywords: JSON.parse(row.keywords),
|
|
511
|
+
transferability: row.transferability,
|
|
512
|
+
createdAt: row.created_at,
|
|
513
|
+
updatedAt: row.updated_at,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function toConceptMember(row) {
|
|
517
|
+
return {
|
|
518
|
+
conceptId: row.concept_id,
|
|
519
|
+
memberType: row.member_type,
|
|
520
|
+
memberId: row.member_id,
|
|
521
|
+
similarityToCentroid: row.similarity_to_centroid,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function toHistoryEntry(row) {
|
|
525
|
+
return {
|
|
526
|
+
cycle: row.cycle,
|
|
527
|
+
totalConcepts: row.total_concepts,
|
|
528
|
+
conceptsByLevel: JSON.parse(row.concepts_by_level),
|
|
529
|
+
newCount: row.new_count,
|
|
530
|
+
mergedCount: row.merged_count,
|
|
531
|
+
avgTransferability: row.avg_transferability,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
//# sourceMappingURL=concept-abstraction.js.map
|