@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.
- package/dist/debate/debate-engine.d.ts +137 -0
- package/dist/debate/debate-engine.js +540 -0
- package/dist/debate/debate-engine.js.map +1 -0
- package/dist/debate/index.d.ts +2 -0
- package/dist/debate/index.js +2 -0
- package/dist/debate/index.js.map +1 -0
- package/dist/emergence/emergence-engine.d.ts +169 -0
- package/dist/emergence/emergence-engine.js +687 -0
- package/dist/emergence/emergence-engine.js.map +1 -0
- package/dist/emergence/index.d.ts +2 -0
- package/dist/emergence/index.js +2 -0
- package/dist/emergence/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/research/research-orchestrator.d.ts +9 -0
- package/dist/research/research-orchestrator.js +91 -0
- package/dist/research/research-orchestrator.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|