@timmeck/brain-core 2.23.0 → 2.25.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.
@@ -0,0 +1,137 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { ThoughtStream } from '../consciousness/thought-stream.js';
3
+ import type { KnowledgeDistiller } from '../research/knowledge-distiller.js';
4
+ import type { HypothesisEngine } from '../hypothesis/engine.js';
5
+ import type { ResearchJournal } from '../research/journal.js';
6
+ import type { AnomalyDetective } from '../research/anomaly-detective.js';
7
+ import type { PredictionEngine } from '../prediction/prediction-engine.js';
8
+ import type { NarrativeEngine } from '../narrative/narrative-engine.js';
9
+ export interface DebateEngineConfig {
10
+ brainName: string;
11
+ /** Domain description for this brain (e.g. "error tracking and code intelligence"). */
12
+ domainDescription?: string;
13
+ }
14
+ export interface DebateDataSources {
15
+ knowledgeDistiller?: KnowledgeDistiller;
16
+ hypothesisEngine?: HypothesisEngine;
17
+ journal?: ResearchJournal;
18
+ anomalyDetective?: AnomalyDetective;
19
+ predictionEngine?: PredictionEngine;
20
+ narrativeEngine?: NarrativeEngine;
21
+ }
22
+ export interface Debate {
23
+ id?: number;
24
+ question: string;
25
+ status: DebateStatus;
26
+ perspectives: DebatePerspective[];
27
+ synthesis: DebateSynthesis | null;
28
+ created_at?: string;
29
+ closed_at?: string;
30
+ }
31
+ export type DebateStatus = 'open' | 'deliberating' | 'synthesized' | 'closed';
32
+ export interface DebatePerspective {
33
+ id?: number;
34
+ debateId?: number;
35
+ brainName: string;
36
+ position: string;
37
+ arguments: DebateArgument[];
38
+ confidence: number;
39
+ relevance: number;
40
+ created_at?: string;
41
+ }
42
+ export interface DebateArgument {
43
+ claim: string;
44
+ evidence: string[];
45
+ source: 'principle' | 'hypothesis' | 'journal' | 'prediction' | 'anomaly' | 'narrative';
46
+ strength: number;
47
+ }
48
+ export interface DebateSynthesis {
49
+ consensus: string | null;
50
+ conflicts: DebateConflict[];
51
+ resolution: string;
52
+ confidence: number;
53
+ recommendations: string[];
54
+ participantCount: number;
55
+ }
56
+ export interface DebateConflict {
57
+ perspectiveA: string;
58
+ perspectiveB: string;
59
+ claimA: string;
60
+ claimB: string;
61
+ resolution: 'a_wins' | 'b_wins' | 'compromise' | 'unresolved';
62
+ reason: string;
63
+ }
64
+ export interface DebateEngineStatus {
65
+ totalDebates: number;
66
+ openDebates: number;
67
+ synthesizedDebates: number;
68
+ avgConfidence: number;
69
+ avgParticipants: number;
70
+ recentDebates: Debate[];
71
+ uptime: number;
72
+ }
73
+ export declare function runDebateMigration(db: Database.Database): void;
74
+ export declare class DebateEngine {
75
+ private readonly db;
76
+ private readonly config;
77
+ private readonly log;
78
+ private ts;
79
+ private sources;
80
+ private startTime;
81
+ private readonly stmtInsertDebate;
82
+ private readonly stmtUpdateDebateStatus;
83
+ private readonly stmtSetSynthesis;
84
+ private readonly stmtInsertPerspective;
85
+ private readonly stmtGetDebate;
86
+ private readonly stmtGetPerspectives;
87
+ private readonly stmtListDebates;
88
+ private readonly stmtTotalDebates;
89
+ private readonly stmtOpenDebates;
90
+ private readonly stmtSynthesizedDebates;
91
+ constructor(db: Database.Database, config: DebateEngineConfig);
92
+ setThoughtStream(stream: ThoughtStream): void;
93
+ setDataSources(sources: DebateDataSources): void;
94
+ /**
95
+ * Start a new debate on a question.
96
+ * Immediately generates this brain's perspective and adds it.
97
+ */
98
+ startDebate(question: string): Debate;
99
+ /**
100
+ * Generate this brain's perspective on a question based on local knowledge.
101
+ * Searches principles, hypotheses, journal, anomalies, predictions.
102
+ */
103
+ generatePerspective(question: string): DebatePerspective;
104
+ /**
105
+ * Add a perspective (from this or another brain) to an existing debate.
106
+ */
107
+ addPerspective(debateId: number, perspective: DebatePerspective): void;
108
+ /**
109
+ * Synthesize all perspectives in a debate: find conflicts, build consensus.
110
+ */
111
+ synthesize(debateId: number): DebateSynthesis | null;
112
+ getDebate(id: number): Debate | null;
113
+ listDebates(limit?: number): Debate[];
114
+ getStatus(): DebateEngineStatus;
115
+ /**
116
+ * Find conflicts between perspectives.
117
+ * Two arguments conflict if they're about the same topic but make opposite claims.
118
+ */
119
+ private findConflicts;
120
+ /**
121
+ * Detect if two arguments conflict.
122
+ * Heuristic: same topic keywords but one has "warning" / negation.
123
+ */
124
+ private argumentsConflict;
125
+ /**
126
+ * Build consensus from perspectives weighted by confidence × relevance.
127
+ */
128
+ private buildConsensus;
129
+ private generateRecommendations;
130
+ private extractKeywords;
131
+ private isRelevant;
132
+ private computeRelevance;
133
+ private generatePosition;
134
+ private loadPerspectives;
135
+ private toDebate;
136
+ private toPerspective;
137
+ }
@@ -0,0 +1,540 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+ // ── Migration ───────────────────────────────────────────
3
+ export function runDebateMigration(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS debates (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ question TEXT NOT NULL,
8
+ status TEXT NOT NULL DEFAULT 'open',
9
+ synthesis_json TEXT,
10
+ created_at TEXT DEFAULT (datetime('now')),
11
+ closed_at TEXT
12
+ );
13
+ CREATE INDEX IF NOT EXISTS idx_debates_status ON debates(status);
14
+
15
+ CREATE TABLE IF NOT EXISTS debate_perspectives (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ debate_id INTEGER NOT NULL,
18
+ brain_name TEXT NOT NULL,
19
+ position TEXT NOT NULL,
20
+ arguments_json TEXT NOT NULL DEFAULT '[]',
21
+ confidence REAL NOT NULL DEFAULT 0,
22
+ relevance REAL NOT NULL DEFAULT 0,
23
+ created_at TEXT DEFAULT (datetime('now')),
24
+ FOREIGN KEY (debate_id) REFERENCES debates(id)
25
+ );
26
+ CREATE INDEX IF NOT EXISTS idx_debate_perspectives_debate ON debate_perspectives(debate_id);
27
+ `);
28
+ }
29
+ // ── Engine ──────────────────────────────────────────────
30
+ export class DebateEngine {
31
+ db;
32
+ config;
33
+ log = getLogger();
34
+ ts = null;
35
+ sources = {};
36
+ startTime = Date.now();
37
+ // Prepared statements
38
+ stmtInsertDebate;
39
+ stmtUpdateDebateStatus;
40
+ stmtSetSynthesis;
41
+ stmtInsertPerspective;
42
+ stmtGetDebate;
43
+ stmtGetPerspectives;
44
+ stmtListDebates;
45
+ stmtTotalDebates;
46
+ stmtOpenDebates;
47
+ stmtSynthesizedDebates;
48
+ constructor(db, config) {
49
+ this.db = db;
50
+ this.config = {
51
+ brainName: config.brainName,
52
+ domainDescription: config.domainDescription ?? config.brainName,
53
+ };
54
+ runDebateMigration(db);
55
+ this.stmtInsertDebate = db.prepare('INSERT INTO debates (question, status) VALUES (?, ?)');
56
+ this.stmtUpdateDebateStatus = db.prepare('UPDATE debates SET status = ?, closed_at = CASE WHEN ? = \'closed\' THEN datetime(\'now\') ELSE closed_at END WHERE id = ?');
57
+ this.stmtSetSynthesis = db.prepare('UPDATE debates SET synthesis_json = ?, status = \'synthesized\' WHERE id = ?');
58
+ this.stmtInsertPerspective = db.prepare('INSERT INTO debate_perspectives (debate_id, brain_name, position, arguments_json, confidence, relevance) VALUES (?, ?, ?, ?, ?, ?)');
59
+ this.stmtGetDebate = db.prepare('SELECT * FROM debates WHERE id = ?');
60
+ this.stmtGetPerspectives = db.prepare('SELECT * FROM debate_perspectives WHERE debate_id = ? ORDER BY confidence DESC');
61
+ this.stmtListDebates = db.prepare('SELECT * FROM debates ORDER BY id DESC LIMIT ?');
62
+ this.stmtTotalDebates = db.prepare('SELECT COUNT(*) as cnt FROM debates');
63
+ this.stmtOpenDebates = db.prepare('SELECT COUNT(*) as cnt FROM debates WHERE status = \'open\' OR status = \'deliberating\'');
64
+ this.stmtSynthesizedDebates = db.prepare('SELECT COUNT(*) as cnt FROM debates WHERE status = \'synthesized\' OR status = \'closed\'');
65
+ this.log.debug(`[DebateEngine] Initialized for ${this.config.brainName}`);
66
+ }
67
+ // ── Setters ──────────────────────────────────────────
68
+ setThoughtStream(stream) { this.ts = stream; }
69
+ setDataSources(sources) {
70
+ this.sources = sources;
71
+ }
72
+ // ── Core: Start a Debate ─────────────────────────────
73
+ /**
74
+ * Start a new debate on a question.
75
+ * Immediately generates this brain's perspective and adds it.
76
+ */
77
+ startDebate(question) {
78
+ this.ts?.emit('debate', 'reflecting', `New debate: "${question.substring(0, 60)}..."`, 'notable');
79
+ const info = this.stmtInsertDebate.run(question, 'open');
80
+ const debateId = Number(info.lastInsertRowid);
81
+ // Generate and add this brain's perspective
82
+ const perspective = this.generatePerspective(question);
83
+ this.addPerspective(debateId, perspective);
84
+ this.stmtUpdateDebateStatus.run('deliberating', 'deliberating', debateId);
85
+ return this.getDebate(debateId);
86
+ }
87
+ // ── Core: Generate Perspective ────────────────────────
88
+ /**
89
+ * Generate this brain's perspective on a question based on local knowledge.
90
+ * Searches principles, hypotheses, journal, anomalies, predictions.
91
+ */
92
+ generatePerspective(question) {
93
+ this.ts?.emit('reflecting', 'analyzing', `Forming perspective on: "${question.substring(0, 50)}..."`, 'routine');
94
+ const args = [];
95
+ const keywords = this.extractKeywords(question);
96
+ // 1. Arguments from principles
97
+ if (this.sources.knowledgeDistiller) {
98
+ try {
99
+ const pkg = this.sources.knowledgeDistiller.getPackage(this.config.brainName);
100
+ for (const p of pkg.principles) {
101
+ if (this.isRelevant(p.statement, keywords)) {
102
+ args.push({
103
+ claim: p.statement,
104
+ evidence: [`principle:${p.id}`, `confidence:${p.confidence.toFixed(2)}`, `samples:${p.sample_size}`],
105
+ source: 'principle',
106
+ strength: p.confidence * Math.min(1, p.sample_size / 10),
107
+ });
108
+ }
109
+ }
110
+ // Also check anti-patterns
111
+ for (const ap of pkg.anti_patterns) {
112
+ if (this.isRelevant(ap.statement, keywords)) {
113
+ args.push({
114
+ claim: `Warning: ${ap.statement}`,
115
+ evidence: [`anti_pattern`, `confidence:${ap.confidence.toFixed(2)}`],
116
+ source: 'principle',
117
+ strength: ap.confidence * 0.8,
118
+ });
119
+ }
120
+ }
121
+ }
122
+ catch { /* not wired */ }
123
+ }
124
+ // 2. Arguments from hypotheses
125
+ if (this.sources.hypothesisEngine) {
126
+ try {
127
+ const confirmed = this.sources.hypothesisEngine.list('confirmed', 30);
128
+ const testing = this.sources.hypothesisEngine.list('testing', 20);
129
+ for (const h of [...confirmed, ...testing]) {
130
+ if (this.isRelevant(h.statement, keywords)) {
131
+ const statusWeight = h.status === 'confirmed' ? 1.0 : 0.6;
132
+ args.push({
133
+ claim: h.statement,
134
+ evidence: [`hypothesis:${h.id}`, `status:${h.status}`, `p_value:${h.p_value.toFixed(4)}`],
135
+ source: 'hypothesis',
136
+ strength: h.confidence * statusWeight,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ catch { /* not wired */ }
142
+ }
143
+ // 3. Arguments from journal
144
+ if (this.sources.journal) {
145
+ try {
146
+ const entries = this.sources.journal.search(question, 15);
147
+ for (const e of entries) {
148
+ if (e.significance === 'breakthrough' || e.significance === 'notable') {
149
+ args.push({
150
+ claim: e.title,
151
+ evidence: [`journal:${e.id}`, `type:${e.type}`, `significance:${e.significance}`],
152
+ source: 'journal',
153
+ strength: e.significance === 'breakthrough' ? 0.9 : 0.6,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ catch { /* not wired */ }
159
+ }
160
+ // 4. Arguments from anomalies
161
+ if (this.sources.anomalyDetective) {
162
+ try {
163
+ const anomalies = this.sources.anomalyDetective.getAnomalies(undefined, 30);
164
+ for (const a of anomalies) {
165
+ if (this.isRelevant(`${a.title} ${a.metric}`, keywords)) {
166
+ args.push({
167
+ claim: `Anomaly detected: ${a.title}`,
168
+ evidence: [`anomaly:${a.id}`, `deviation:${a.deviation.toFixed(2)}`, `severity:${a.severity}`],
169
+ source: 'anomaly',
170
+ strength: Math.min(1, 0.3 + Math.abs(a.deviation) * 0.1),
171
+ });
172
+ }
173
+ }
174
+ }
175
+ catch { /* not wired */ }
176
+ }
177
+ // 5. Arguments from predictions
178
+ if (this.sources.predictionEngine) {
179
+ try {
180
+ const summary = this.sources.predictionEngine.getSummary();
181
+ if (summary.accuracy_rate > 0 && this.isRelevant(JSON.stringify(summary), keywords)) {
182
+ args.push({
183
+ claim: `Prediction track record: ${(summary.accuracy_rate * 100).toFixed(0)}% accuracy over ${summary.total_predictions} predictions`,
184
+ evidence: [`predictions:${summary.total_predictions}`, `accuracy:${summary.accuracy_rate.toFixed(2)}`],
185
+ source: 'prediction',
186
+ strength: summary.accuracy_rate,
187
+ });
188
+ }
189
+ }
190
+ catch { /* not wired */ }
191
+ }
192
+ // 6. Narrative explanation
193
+ if (this.sources.narrativeEngine) {
194
+ try {
195
+ const explanation = this.sources.narrativeEngine.explain(question);
196
+ if (explanation.details.length > 0) {
197
+ args.push({
198
+ claim: explanation.summary.substring(0, 200),
199
+ evidence: [`narrative:${explanation.topic}`],
200
+ source: 'narrative',
201
+ strength: explanation.confidence,
202
+ });
203
+ }
204
+ }
205
+ catch { /* not wired */ }
206
+ }
207
+ // Sort by strength
208
+ args.sort((a, b) => b.strength - a.strength);
209
+ // Compute overall confidence and relevance
210
+ const confidence = args.length > 0
211
+ ? args.reduce((sum, a) => sum + a.strength, 0) / args.length
212
+ : 0;
213
+ const relevance = this.computeRelevance(question, args);
214
+ // Generate position: summarize top arguments
215
+ const position = this.generatePosition(question, args);
216
+ this.ts?.emit('reflecting', 'analyzing', `Perspective formed: ${args.length} arguments, confidence=${(confidence * 100).toFixed(0)}%`, 'routine');
217
+ return {
218
+ brainName: this.config.brainName,
219
+ position,
220
+ arguments: args.slice(0, 10), // Top 10
221
+ confidence,
222
+ relevance,
223
+ };
224
+ }
225
+ // ── Core: Add External Perspective ────────────────────
226
+ /**
227
+ * Add a perspective (from this or another brain) to an existing debate.
228
+ */
229
+ addPerspective(debateId, perspective) {
230
+ this.stmtInsertPerspective.run(debateId, perspective.brainName, perspective.position, JSON.stringify(perspective.arguments), perspective.confidence, perspective.relevance);
231
+ this.ts?.emit('debate', 'reflecting', `${perspective.brainName} added perspective (confidence=${(perspective.confidence * 100).toFixed(0)}%)`, 'routine');
232
+ }
233
+ // ── Core: Synthesize ──────────────────────────────────
234
+ /**
235
+ * Synthesize all perspectives in a debate: find conflicts, build consensus.
236
+ */
237
+ synthesize(debateId) {
238
+ const debate = this.getDebate(debateId);
239
+ if (!debate)
240
+ return null;
241
+ if (debate.perspectives.length === 0)
242
+ return null;
243
+ this.ts?.emit('debate', 'analyzing', `Synthesizing debate: "${debate.question.substring(0, 40)}..." (${debate.perspectives.length} perspectives)`, 'notable');
244
+ // 1. Find conflicts between perspectives
245
+ const conflicts = this.findConflicts(debate.perspectives);
246
+ // 2. Build weighted consensus
247
+ const consensus = this.buildConsensus(debate.perspectives, conflicts);
248
+ // 3. Generate recommendations
249
+ const recommendations = this.generateRecommendations(debate.perspectives, conflicts);
250
+ // 4. Compute overall confidence
251
+ const totalWeight = debate.perspectives.reduce((s, p) => s + p.confidence * p.relevance, 0);
252
+ const totalRelevance = debate.perspectives.reduce((s, p) => s + p.relevance, 0);
253
+ const avgConfidence = totalRelevance > 0 ? totalWeight / totalRelevance : 0;
254
+ const synthesis = {
255
+ consensus,
256
+ conflicts,
257
+ resolution: conflicts.length === 0
258
+ ? 'All perspectives align — strong consensus.'
259
+ : `${conflicts.length} conflict(s) found. ${conflicts.filter(c => c.resolution !== 'unresolved').length} resolved.`,
260
+ confidence: avgConfidence,
261
+ recommendations,
262
+ participantCount: debate.perspectives.length,
263
+ };
264
+ // Persist
265
+ this.stmtSetSynthesis.run(JSON.stringify(synthesis), debateId);
266
+ this.ts?.emit('debate', 'discovering', `Debate synthesized: ${conflicts.length} conflicts, confidence=${(avgConfidence * 100).toFixed(0)}%`, conflicts.length > 0 ? 'notable' : 'routine');
267
+ return synthesis;
268
+ }
269
+ // ── Query Methods ────────────────────────────────────
270
+ getDebate(id) {
271
+ const row = this.stmtGetDebate.get(id);
272
+ if (!row)
273
+ return null;
274
+ const perspectives = this.loadPerspectives(id);
275
+ return this.toDebate(row, perspectives);
276
+ }
277
+ listDebates(limit = 20) {
278
+ const rows = this.stmtListDebates.all(limit);
279
+ return rows.map(r => {
280
+ const perspectives = this.loadPerspectives(r.id);
281
+ return this.toDebate(r, perspectives);
282
+ });
283
+ }
284
+ getStatus() {
285
+ const total = this.stmtTotalDebates.get().cnt;
286
+ const open = this.stmtOpenDebates.get().cnt;
287
+ const synthesized = this.stmtSynthesizedDebates.get().cnt;
288
+ const recent = this.listDebates(5);
289
+ const syntheses = recent
290
+ .filter(d => d.synthesis)
291
+ .map(d => d.synthesis);
292
+ const avgConfidence = syntheses.length > 0
293
+ ? syntheses.reduce((s, syn) => s + syn.confidence, 0) / syntheses.length
294
+ : 0;
295
+ const avgParticipants = syntheses.length > 0
296
+ ? syntheses.reduce((s, syn) => s + syn.participantCount, 0) / syntheses.length
297
+ : 0;
298
+ return {
299
+ totalDebates: total,
300
+ openDebates: open,
301
+ synthesizedDebates: synthesized,
302
+ avgConfidence,
303
+ avgParticipants,
304
+ recentDebates: recent,
305
+ uptime: Date.now() - this.startTime,
306
+ };
307
+ }
308
+ // ── Private: Conflict Detection ────────────────────────
309
+ /**
310
+ * Find conflicts between perspectives.
311
+ * Two arguments conflict if they're about the same topic but make opposite claims.
312
+ */
313
+ findConflicts(perspectives) {
314
+ const conflicts = [];
315
+ for (let i = 0; i < perspectives.length; i++) {
316
+ for (let j = i + 1; j < perspectives.length; j++) {
317
+ const pA = perspectives[i];
318
+ const pB = perspectives[j];
319
+ // Compare each argument pair
320
+ for (const argA of pA.arguments) {
321
+ for (const argB of pB.arguments) {
322
+ if (this.argumentsConflict(argA, argB)) {
323
+ // Resolve: higher confidence × relevance wins
324
+ const weightA = pA.confidence * pA.relevance * argA.strength;
325
+ const weightB = pB.confidence * pB.relevance * argB.strength;
326
+ let resolution;
327
+ let reason;
328
+ if (Math.abs(weightA - weightB) < 0.1) {
329
+ resolution = 'compromise';
330
+ reason = `Both sides have similar weight (${weightA.toFixed(2)} vs ${weightB.toFixed(2)}). Consider both perspectives.`;
331
+ }
332
+ else if (weightA > weightB) {
333
+ resolution = 'a_wins';
334
+ reason = `${pA.brainName}'s argument is stronger (weight: ${weightA.toFixed(2)} vs ${weightB.toFixed(2)}) based on confidence and evidence.`;
335
+ }
336
+ else {
337
+ resolution = 'b_wins';
338
+ reason = `${pB.brainName}'s argument is stronger (weight: ${weightB.toFixed(2)} vs ${weightA.toFixed(2)}) based on confidence and evidence.`;
339
+ }
340
+ conflicts.push({
341
+ perspectiveA: pA.brainName,
342
+ perspectiveB: pB.brainName,
343
+ claimA: argA.claim.substring(0, 150),
344
+ claimB: argB.claim.substring(0, 150),
345
+ resolution,
346
+ reason,
347
+ });
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ return conflicts;
354
+ }
355
+ /**
356
+ * Detect if two arguments conflict.
357
+ * Heuristic: same topic keywords but one has "warning" / negation.
358
+ */
359
+ argumentsConflict(a, b) {
360
+ const wordsA = new Set(a.claim.toLowerCase().split(/\s+/).filter(w => w.length > 3));
361
+ const wordsB = new Set(b.claim.toLowerCase().split(/\s+/).filter(w => w.length > 3));
362
+ // Must have some topic overlap
363
+ let overlap = 0;
364
+ for (const w of wordsA)
365
+ if (wordsB.has(w))
366
+ overlap++;
367
+ const overlapRatio = overlap / Math.max(wordsA.size, wordsB.size, 1);
368
+ if (overlapRatio < 0.2)
369
+ return false;
370
+ // Check for opposing signals
371
+ const negations = ['not', 'never', 'warning', 'avoid', 'decrease', 'reduce', 'lower', 'bad', 'risk', 'danger'];
372
+ const hasNegA = negations.some(n => a.claim.toLowerCase().includes(n));
373
+ const hasNegB = negations.some(n => b.claim.toLowerCase().includes(n));
374
+ // One positive, one negative about same topic = conflict
375
+ if (hasNegA !== hasNegB)
376
+ return true;
377
+ // Different sources about same topic with very different strengths
378
+ if (a.source !== b.source && overlapRatio > 0.3 && Math.abs(a.strength - b.strength) > 0.4) {
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+ // ── Private: Consensus Building ────────────────────────
384
+ /**
385
+ * Build consensus from perspectives weighted by confidence × relevance.
386
+ */
387
+ buildConsensus(perspectives, conflicts) {
388
+ if (perspectives.length === 0)
389
+ return null;
390
+ if (perspectives.length === 1)
391
+ return perspectives[0].position;
392
+ // Collect all non-conflicting claims
393
+ const conflictingClaims = new Set();
394
+ for (const c of conflicts) {
395
+ conflictingClaims.add(c.claimA.substring(0, 50));
396
+ conflictingClaims.add(c.claimB.substring(0, 50));
397
+ }
398
+ // Aggregate non-conflicting arguments by weight
399
+ const weightedClaims = [];
400
+ for (const p of perspectives) {
401
+ for (const arg of p.arguments) {
402
+ const key = arg.claim.substring(0, 50);
403
+ if (!conflictingClaims.has(key)) {
404
+ weightedClaims.push({
405
+ claim: arg.claim,
406
+ weight: p.confidence * p.relevance * arg.strength,
407
+ brain: p.brainName,
408
+ });
409
+ }
410
+ }
411
+ }
412
+ weightedClaims.sort((a, b) => b.weight - a.weight);
413
+ // Build consensus from top claims
414
+ const topClaims = weightedClaims.slice(0, 5);
415
+ if (topClaims.length === 0) {
416
+ // Only conflicts, no agreement
417
+ return `No consensus reached. ${conflicts.length} conflicting viewpoints from ${perspectives.map(p => p.brainName).join(', ')}.`;
418
+ }
419
+ const parts = topClaims.map(c => c.claim);
420
+ const participants = [...new Set(perspectives.map(p => p.brainName))].join(', ');
421
+ return `Consensus from ${participants}: ${parts.join('. ')}.`;
422
+ }
423
+ // ── Private: Recommendations ──────────────────────────
424
+ generateRecommendations(perspectives, conflicts) {
425
+ const recs = [];
426
+ // High-confidence unanimous arguments → strong recommendation
427
+ const allArgs = perspectives.flatMap(p => p.arguments.map(a => ({ ...a, brain: p.brainName, pConfidence: p.confidence })));
428
+ // Find arguments that appear in multiple perspectives
429
+ const claimCounts = new Map();
430
+ for (const a of allArgs) {
431
+ const key = a.claim.substring(0, 60).toLowerCase();
432
+ const existing = claimCounts.get(key) ?? { count: 0, totalStrength: 0, brains: [] };
433
+ existing.count++;
434
+ existing.totalStrength += a.strength * a.pConfidence;
435
+ existing.brains.push(a.brain);
436
+ claimCounts.set(key, existing);
437
+ }
438
+ // Multi-brain agreement = strong recommendation
439
+ for (const [, info] of claimCounts) {
440
+ const uniqueBrains = [...new Set(info.brains)];
441
+ if (uniqueBrains.length > 1) {
442
+ recs.push(`Strong: ${uniqueBrains.join(' + ')} agree on this point (combined strength: ${info.totalStrength.toFixed(2)}).`);
443
+ }
444
+ }
445
+ // Unresolved conflicts → investigate
446
+ const unresolved = conflicts.filter(c => c.resolution === 'unresolved');
447
+ if (unresolved.length > 0) {
448
+ recs.push(`Investigate: ${unresolved.length} unresolved conflict(s) need more data.`);
449
+ }
450
+ // Low confidence → gather more evidence
451
+ const lowConf = perspectives.filter(p => p.confidence < 0.3);
452
+ if (lowConf.length > 0) {
453
+ recs.push(`Low confidence from ${lowConf.map(p => p.brainName).join(', ')} — more data needed in these domains.`);
454
+ }
455
+ // If no recommendations, note it
456
+ if (recs.length === 0) {
457
+ recs.push('All perspectives considered. Act on the consensus with measured confidence.');
458
+ }
459
+ return recs;
460
+ }
461
+ // ── Private: Helpers ──────────────────────────────────
462
+ extractKeywords(text) {
463
+ const stopwords = new Set([
464
+ 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but', 'in',
465
+ 'with', 'to', 'for', 'of', 'not', 'no', 'can', 'will', 'do', 'does',
466
+ 'was', 'were', 'has', 'have', 'had', 'this', 'that', 'from', 'are',
467
+ 'der', 'die', 'das', 'und', 'oder', 'aber', 'ist', 'sind', 'ein', 'eine',
468
+ 'für', 'mit', 'auf', 'bei', 'nach', 'von', 'wie', 'was', 'wir', 'ich',
469
+ 'warum', 'wann', 'wenn', 'als', 'auch', 'noch', 'nur', 'mehr', 'sehr',
470
+ 'should', 'would', 'could', 'how', 'why', 'what', 'when', 'where', 'who',
471
+ ]);
472
+ return text.toLowerCase()
473
+ .split(/\s+/)
474
+ .filter(w => w.length > 2 && !stopwords.has(w))
475
+ .map(w => w.replace(/[^a-z0-9äöüß-]/g, ''))
476
+ .filter(w => w.length > 2);
477
+ }
478
+ isRelevant(text, keywords) {
479
+ const lower = text.toLowerCase();
480
+ const matches = keywords.filter(k => lower.includes(k));
481
+ return matches.length >= 1;
482
+ }
483
+ computeRelevance(question, args) {
484
+ if (args.length === 0)
485
+ return 0.1; // Minimal relevance
486
+ // More arguments = more relevant domain
487
+ const argScore = Math.min(1, args.length / 5);
488
+ // Higher average strength = more relevant
489
+ const avgStrength = args.reduce((s, a) => s + a.strength, 0) / args.length;
490
+ return (argScore * 0.5 + avgStrength * 0.5);
491
+ }
492
+ generatePosition(question, args) {
493
+ if (args.length === 0) {
494
+ return `${this.config.brainName} has limited knowledge about this topic.`;
495
+ }
496
+ const topArgs = args.slice(0, 3);
497
+ const domain = this.config.domainDescription ?? this.config.brainName;
498
+ const claims = topArgs.map(a => a.claim).join('; ');
499
+ return `From ${domain} perspective: ${claims}`;
500
+ }
501
+ loadPerspectives(debateId) {
502
+ const rows = this.stmtGetPerspectives.all(debateId);
503
+ return rows.map(r => this.toPerspective(r));
504
+ }
505
+ toDebate(row, perspectives) {
506
+ let synthesis = null;
507
+ try {
508
+ if (row.synthesis_json)
509
+ synthesis = JSON.parse(row.synthesis_json);
510
+ }
511
+ catch { /* ignore */ }
512
+ return {
513
+ id: row.id,
514
+ question: row.question,
515
+ status: row.status,
516
+ perspectives,
517
+ synthesis,
518
+ created_at: row.created_at,
519
+ closed_at: row.closed_at,
520
+ };
521
+ }
522
+ toPerspective(row) {
523
+ let args = [];
524
+ try {
525
+ args = JSON.parse(row.arguments_json || '[]');
526
+ }
527
+ catch { /* ignore */ }
528
+ return {
529
+ id: row.id,
530
+ debateId: row.debate_id,
531
+ brainName: row.brain_name,
532
+ position: row.position,
533
+ arguments: args,
534
+ confidence: row.confidence,
535
+ relevance: row.relevance,
536
+ created_at: row.created_at,
537
+ };
538
+ }
539
+ }
540
+ //# sourceMappingURL=debate-engine.js.map