@timmeck/brain-core 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.d.ts +20 -0
  2. package/dist/index.js +20 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/research/adaptive-strategy.d.ts +56 -0
  5. package/dist/research/adaptive-strategy.js +236 -0
  6. package/dist/research/adaptive-strategy.js.map +1 -0
  7. package/dist/research/agenda-engine.d.ts +46 -0
  8. package/dist/research/agenda-engine.js +264 -0
  9. package/dist/research/agenda-engine.js.map +1 -0
  10. package/dist/research/anomaly-detective.d.ts +62 -0
  11. package/dist/research/anomaly-detective.js +318 -0
  12. package/dist/research/anomaly-detective.js.map +1 -0
  13. package/dist/research/autonomous-scheduler.d.ts +124 -0
  14. package/dist/research/autonomous-scheduler.js +411 -0
  15. package/dist/research/autonomous-scheduler.js.map +1 -0
  16. package/dist/research/counterfactual-engine.d.ts +63 -0
  17. package/dist/research/counterfactual-engine.js +263 -0
  18. package/dist/research/counterfactual-engine.js.map +1 -0
  19. package/dist/research/cross-domain-engine.d.ts +52 -0
  20. package/dist/research/cross-domain-engine.js +283 -0
  21. package/dist/research/cross-domain-engine.js.map +1 -0
  22. package/dist/research/experiment-engine.d.ts +77 -0
  23. package/dist/research/experiment-engine.js +328 -0
  24. package/dist/research/experiment-engine.js.map +1 -0
  25. package/dist/research/journal.d.ts +62 -0
  26. package/dist/research/journal.js +262 -0
  27. package/dist/research/journal.js.map +1 -0
  28. package/dist/research/knowledge-distiller.d.ts +95 -0
  29. package/dist/research/knowledge-distiller.js +426 -0
  30. package/dist/research/knowledge-distiller.js.map +1 -0
  31. package/dist/research/self-observer.d.ts +55 -0
  32. package/dist/research/self-observer.js +268 -0
  33. package/dist/research/self-observer.js.map +1 -0
  34. package/package.json +2 -1
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,264 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+ // ── Migration ───────────────────────────────────────────
3
+ export function runAgendaMigration(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS research_agenda (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ priority REAL NOT NULL,
8
+ question TEXT NOT NULL,
9
+ type TEXT NOT NULL,
10
+ estimated_cycles INTEGER NOT NULL,
11
+ expected_impact TEXT NOT NULL,
12
+ prerequisites TEXT NOT NULL DEFAULT '[]',
13
+ auto_executable INTEGER NOT NULL DEFAULT 0,
14
+ status TEXT NOT NULL DEFAULT 'open',
15
+ created_at TEXT DEFAULT (datetime('now')),
16
+ completed_at TEXT
17
+ );
18
+ CREATE INDEX IF NOT EXISTS idx_agenda_status ON research_agenda(status);
19
+ CREATE INDEX IF NOT EXISTS idx_agenda_priority ON research_agenda(priority DESC);
20
+ `);
21
+ }
22
+ // ── Engine ──────────────────────────────────────────────
23
+ export class ResearchAgendaEngine {
24
+ db;
25
+ config;
26
+ log = getLogger();
27
+ constructor(db, config) {
28
+ this.db = db;
29
+ this.config = {
30
+ brainName: config.brainName,
31
+ maxItems: config.maxItems ?? 50,
32
+ };
33
+ runAgendaMigration(db);
34
+ }
35
+ /** Generate research agenda from current state of knowledge. */
36
+ generate() {
37
+ const items = [];
38
+ // 1. Unconfirmed hypotheses → need testing
39
+ items.push(...this.findOpenHypotheses());
40
+ // 2. Knowledge gaps → areas with little data
41
+ items.push(...this.findKnowledgeGaps());
42
+ // 3. Unexplored correlations
43
+ items.push(...this.findUnexploredCorrelations());
44
+ // 4. Anomalies needing investigation
45
+ items.push(...this.findOpenAnomalies());
46
+ // 5. Parameter optimization opportunities
47
+ items.push(...this.findParameterSweeps());
48
+ // Deduplicate and persist
49
+ for (const item of items) {
50
+ this.upsertItem(item);
51
+ }
52
+ // Cleanup old completed/dismissed items
53
+ this.db.prepare(`
54
+ DELETE FROM research_agenda WHERE status IN ('completed', 'dismissed')
55
+ AND id NOT IN (SELECT id FROM research_agenda ORDER BY completed_at DESC LIMIT 20)
56
+ `).run();
57
+ return this.getAgenda();
58
+ }
59
+ /** Get the prioritized research agenda. */
60
+ getAgenda(limit = 20) {
61
+ return this.db.prepare(`
62
+ SELECT * FROM research_agenda WHERE status IN ('open', 'in_progress')
63
+ ORDER BY priority DESC LIMIT ?
64
+ `).all(limit).map(r => this.rowToItem(r));
65
+ }
66
+ /** Get the single most important next research question. */
67
+ getNext() {
68
+ const row = this.db.prepare(`
69
+ SELECT * FROM research_agenda WHERE status = 'open'
70
+ ORDER BY priority DESC LIMIT 1
71
+ `).get();
72
+ return row ? this.rowToItem(row) : null;
73
+ }
74
+ /** Reprioritize an agenda item. */
75
+ setPriority(id, priority) {
76
+ const result = this.db.prepare(`
77
+ UPDATE research_agenda SET priority = ? WHERE id = ?
78
+ `).run(Math.max(0, Math.min(1, priority)), id);
79
+ return result.changes > 0;
80
+ }
81
+ /** Add a user-defined research question. */
82
+ ask(question, type = 'hypothesis_test') {
83
+ const result = this.db.prepare(`
84
+ INSERT INTO research_agenda (priority, question, type, estimated_cycles, expected_impact, auto_executable, status)
85
+ VALUES (?, ?, ?, ?, ?, ?, 'open')
86
+ `).run(0.9, question, type, 5, 'User-requested investigation', 0);
87
+ return this.getById(Number(result.lastInsertRowid));
88
+ }
89
+ /** Mark an item as completed or dismissed. */
90
+ resolve(id, status) {
91
+ const result = this.db.prepare(`
92
+ UPDATE research_agenda SET status = ?, completed_at = datetime('now') WHERE id = ?
93
+ `).run(status, id);
94
+ return result.changes > 0;
95
+ }
96
+ getById(id) {
97
+ const row = this.db.prepare(`SELECT * FROM research_agenda WHERE id = ?`).get(id);
98
+ return row ? this.rowToItem(row) : null;
99
+ }
100
+ findOpenHypotheses() {
101
+ const items = [];
102
+ try {
103
+ const pending = this.db.prepare(`
104
+ SELECT COUNT(*) as c FROM hypotheses WHERE status IN ('proposed', 'testing')
105
+ `).get();
106
+ if (pending.c > 0) {
107
+ items.push({
108
+ priority: 0.7,
109
+ question: `${pending.c} hypotheses pending testing. Run hypothesis tests to confirm or reject.`,
110
+ type: 'hypothesis_test',
111
+ estimated_cycles: Math.ceil(pending.c / 3),
112
+ expected_impact: 'Validate or reject pending theories about system behavior.',
113
+ prerequisites: [],
114
+ auto_executable: true,
115
+ status: 'open',
116
+ });
117
+ }
118
+ }
119
+ catch { /* table might not exist */ }
120
+ return items;
121
+ }
122
+ findKnowledgeGaps() {
123
+ const items = [];
124
+ try {
125
+ // Find event types with few observations
126
+ const sparse = this.db.prepare(`
127
+ SELECT type, COUNT(*) as c FROM causal_events
128
+ GROUP BY type HAVING c < 10
129
+ ORDER BY c ASC LIMIT 5
130
+ `).all();
131
+ for (const s of sparse) {
132
+ items.push({
133
+ priority: 0.5 + (1 - s.c / 10) * 0.3,
134
+ question: `Only ${s.c} observations of "${s.type}". Need more data for reliable causal analysis.`,
135
+ type: 'knowledge_gap',
136
+ estimated_cycles: 10,
137
+ expected_impact: `More data about "${s.type}" enables better causal inference.`,
138
+ prerequisites: [],
139
+ auto_executable: false,
140
+ status: 'open',
141
+ });
142
+ }
143
+ }
144
+ catch { /* table might not exist */ }
145
+ return items;
146
+ }
147
+ findUnexploredCorrelations() {
148
+ const items = [];
149
+ try {
150
+ const eventTypes = this.db.prepare(`
151
+ SELECT DISTINCT type FROM causal_events ORDER BY type
152
+ `).all();
153
+ const existingEdges = new Set();
154
+ try {
155
+ const edges = this.db.prepare(`SELECT cause, effect FROM causal_edges`).all();
156
+ for (const e of edges)
157
+ existingEdges.add(`${e.cause}→${e.effect}`);
158
+ }
159
+ catch { /* ignore */ }
160
+ // Find pairs not yet analyzed
161
+ let unexplored = 0;
162
+ for (let i = 0; i < eventTypes.length && unexplored < 3; i++) {
163
+ for (let j = i + 1; j < eventTypes.length; j++) {
164
+ const key = `${eventTypes[i].type}→${eventTypes[j].type}`;
165
+ const keyRev = `${eventTypes[j].type}→${eventTypes[i].type}`;
166
+ if (!existingEdges.has(key) && !existingEdges.has(keyRev)) {
167
+ unexplored++;
168
+ if (unexplored <= 3) {
169
+ items.push({
170
+ priority: 0.4,
171
+ question: `Is there a causal relationship between "${eventTypes[i].type}" and "${eventTypes[j].type}"?`,
172
+ type: 'correlation_search',
173
+ estimated_cycles: 1,
174
+ expected_impact: 'Discover hidden relationships between event types.',
175
+ prerequisites: [],
176
+ auto_executable: true,
177
+ status: 'open',
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ catch { /* table might not exist */ }
185
+ return items;
186
+ }
187
+ findOpenAnomalies() {
188
+ const items = [];
189
+ try {
190
+ const anomalies = this.db.prepare(`
191
+ SELECT type, title FROM self_insights
192
+ WHERE type = 'anomaly' AND timestamp > ?
193
+ ORDER BY timestamp DESC LIMIT 3
194
+ `).all(Date.now() - 86_400_000 * 7);
195
+ for (const a of anomalies) {
196
+ items.push({
197
+ priority: 0.8,
198
+ question: `Investigate anomaly: ${a.title}`,
199
+ type: 'anomaly_investigation',
200
+ estimated_cycles: 3,
201
+ expected_impact: 'Understanding anomalies prevents degradation.',
202
+ prerequisites: [],
203
+ auto_executable: false,
204
+ status: 'open',
205
+ });
206
+ }
207
+ }
208
+ catch { /* table might not exist */ }
209
+ return items;
210
+ }
211
+ findParameterSweeps() {
212
+ const items = [];
213
+ try {
214
+ const underOptimized = this.db.prepare(`
215
+ SELECT strategy, parameter, value, min_value, max_value
216
+ FROM strategy_parameters
217
+ WHERE parameter NOT IN (
218
+ SELECT DISTINCT parameter FROM strategy_adaptations WHERE timestamp > ?
219
+ )
220
+ `).all(Date.now() - 86_400_000 * 14);
221
+ if (underOptimized.length > 0) {
222
+ items.push({
223
+ priority: 0.5,
224
+ question: `${underOptimized.length} parameters haven't been optimized in 2+ weeks. Run parameter sweep.`,
225
+ type: 'parameter_sweep',
226
+ estimated_cycles: underOptimized.length * 2,
227
+ expected_impact: 'Stale parameters may be suboptimal. Re-optimization could improve performance.',
228
+ prerequisites: [],
229
+ auto_executable: true,
230
+ status: 'open',
231
+ });
232
+ }
233
+ }
234
+ catch { /* table might not exist */ }
235
+ return items;
236
+ }
237
+ upsertItem(item) {
238
+ // Check for similar existing items
239
+ const existing = this.db.prepare(`
240
+ SELECT id FROM research_agenda WHERE question = ? AND status = 'open' LIMIT 1
241
+ `).get(item.question);
242
+ if (existing)
243
+ return;
244
+ this.db.prepare(`
245
+ INSERT INTO research_agenda (priority, question, type, estimated_cycles, expected_impact, prerequisites, auto_executable, status)
246
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
247
+ `).run(item.priority, item.question, item.type, item.estimated_cycles, item.expected_impact, JSON.stringify(item.prerequisites), item.auto_executable ? 1 : 0, item.status);
248
+ }
249
+ rowToItem(row) {
250
+ return {
251
+ id: row.id,
252
+ priority: row.priority,
253
+ question: row.question,
254
+ type: row.type,
255
+ estimated_cycles: row.estimated_cycles,
256
+ expected_impact: row.expected_impact,
257
+ prerequisites: JSON.parse(row.prerequisites || '[]'),
258
+ auto_executable: row.auto_executable === 1,
259
+ status: row.status,
260
+ created_at: row.created_at,
261
+ };
262
+ }
263
+ }
264
+ //# sourceMappingURL=agenda-engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agenda-engine.js","sourceRoot":"","sources":["../../src/research/agenda-engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type Database from 'better-sqlite3';
2
+ export type AnomalyType = 'statistical' | 'behavioral' | 'causal' | 'cross_domain' | 'drift';
3
+ export type AnomalySeverity = 'low' | 'medium' | 'high' | 'critical';
4
+ export interface Anomaly {
5
+ id?: number;
6
+ timestamp: number;
7
+ type: AnomalyType;
8
+ severity: AnomalySeverity;
9
+ title: string;
10
+ description: string;
11
+ metric: string;
12
+ expected_value: number;
13
+ actual_value: number;
14
+ deviation: number;
15
+ evidence: Record<string, unknown>;
16
+ resolved: boolean;
17
+ resolution?: string;
18
+ }
19
+ export interface DriftReport {
20
+ metric: string;
21
+ direction: 'increasing' | 'decreasing' | 'stable';
22
+ rate_per_day: number;
23
+ cumulative_change: number;
24
+ period_days: number;
25
+ significant: boolean;
26
+ description: string;
27
+ }
28
+ export interface AnomalyDetectiveConfig {
29
+ brainName: string;
30
+ /** Z-score threshold for statistical anomalies. Default: 2.0 */
31
+ zThreshold?: number;
32
+ /** EWMA smoothing factor (0-1). Default: 0.3 */
33
+ ewmaAlpha?: number;
34
+ /** Minimum data points for drift detection. Default: 7 */
35
+ minDriftPoints?: number;
36
+ }
37
+ export declare function runAnomalyDetectiveMigration(db: Database.Database): void;
38
+ export declare class AnomalyDetective {
39
+ private db;
40
+ private config;
41
+ private log;
42
+ constructor(db: Database.Database, config: AnomalyDetectiveConfig);
43
+ /** Record a metric value for anomaly tracking. */
44
+ recordMetric(metric: string, value: number): void;
45
+ /** Check all tracked metrics for anomalies. */
46
+ detect(): Anomaly[];
47
+ /** Get current anomalies. */
48
+ getAnomalies(type?: AnomalyType, limit?: number): Anomaly[];
49
+ /** Investigate a specific anomaly — provide context and potential causes. */
50
+ investigate(anomalyId: number): Record<string, unknown> | null;
51
+ /** Get anomaly history (including resolved). */
52
+ getHistory(limit?: number): Anomaly[];
53
+ /** Get drift reports for all tracked metrics. */
54
+ getDriftReport(): DriftReport[];
55
+ /** Resolve an anomaly. */
56
+ resolve(anomalyId: number, resolution: string): boolean;
57
+ private detectStatisticalAnomaly;
58
+ private detectDrift;
59
+ private computeDriftReport;
60
+ private persistAnomaly;
61
+ private rowToAnomaly;
62
+ }
@@ -0,0 +1,318 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+ // ── Migration ───────────────────────────────────────────
3
+ export function runAnomalyDetectiveMigration(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS anomalies (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ timestamp INTEGER NOT NULL,
8
+ type TEXT NOT NULL,
9
+ severity TEXT NOT NULL,
10
+ title TEXT NOT NULL,
11
+ description TEXT NOT NULL,
12
+ metric TEXT NOT NULL,
13
+ expected_value REAL NOT NULL,
14
+ actual_value REAL NOT NULL,
15
+ deviation REAL NOT NULL,
16
+ evidence TEXT NOT NULL,
17
+ resolved INTEGER DEFAULT 0,
18
+ resolution TEXT,
19
+ created_at TEXT DEFAULT (datetime('now'))
20
+ );
21
+ CREATE INDEX IF NOT EXISTS idx_anomalies_type ON anomalies(type);
22
+ CREATE INDEX IF NOT EXISTS idx_anomalies_ts ON anomalies(timestamp);
23
+ CREATE INDEX IF NOT EXISTS idx_anomalies_resolved ON anomalies(resolved);
24
+
25
+ CREATE TABLE IF NOT EXISTS metric_history (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ metric TEXT NOT NULL,
28
+ value REAL NOT NULL,
29
+ timestamp INTEGER NOT NULL,
30
+ created_at TEXT DEFAULT (datetime('now'))
31
+ );
32
+ CREATE INDEX IF NOT EXISTS idx_metric_hist ON metric_history(metric, timestamp);
33
+ `);
34
+ }
35
+ // ── Engine ──────────────────────────────────────────────
36
+ export class AnomalyDetective {
37
+ db;
38
+ config;
39
+ log = getLogger();
40
+ constructor(db, config) {
41
+ this.db = db;
42
+ this.config = {
43
+ brainName: config.brainName,
44
+ zThreshold: config.zThreshold ?? 2.0,
45
+ ewmaAlpha: config.ewmaAlpha ?? 0.3,
46
+ minDriftPoints: config.minDriftPoints ?? 7,
47
+ };
48
+ runAnomalyDetectiveMigration(db);
49
+ }
50
+ /** Record a metric value for anomaly tracking. */
51
+ recordMetric(metric, value) {
52
+ this.db.prepare(`
53
+ INSERT INTO metric_history (metric, value, timestamp)
54
+ VALUES (?, ?, ?)
55
+ `).run(metric, value, Date.now());
56
+ }
57
+ /** Check all tracked metrics for anomalies. */
58
+ detect() {
59
+ const anomalies = [];
60
+ const metrics = this.db.prepare(`
61
+ SELECT DISTINCT metric FROM metric_history
62
+ `).all();
63
+ for (const { metric } of metrics) {
64
+ const values = this.db.prepare(`
65
+ SELECT value, timestamp FROM metric_history
66
+ WHERE metric = ? ORDER BY timestamp ASC
67
+ `).all(metric);
68
+ if (values.length < 3)
69
+ continue;
70
+ // Statistical anomaly detection (Z-score)
71
+ const statistical = this.detectStatisticalAnomaly(metric, values);
72
+ if (statistical)
73
+ anomalies.push(statistical);
74
+ // Drift detection (EWMA)
75
+ const drift = this.detectDrift(metric, values);
76
+ if (drift)
77
+ anomalies.push(drift);
78
+ }
79
+ // Persist new anomalies
80
+ for (const a of anomalies) {
81
+ this.persistAnomaly(a);
82
+ }
83
+ return anomalies;
84
+ }
85
+ /** Get current anomalies. */
86
+ getAnomalies(type, limit = 20) {
87
+ let sql = `SELECT * FROM anomalies`;
88
+ const params = [];
89
+ if (type) {
90
+ sql += ` WHERE type = ?`;
91
+ params.push(type);
92
+ }
93
+ sql += ` ORDER BY timestamp DESC LIMIT ?`;
94
+ params.push(limit);
95
+ return this.db.prepare(sql).all(...params).map(r => this.rowToAnomaly(r));
96
+ }
97
+ /** Investigate a specific anomaly — provide context and potential causes. */
98
+ investigate(anomalyId) {
99
+ const anomaly = this.db.prepare(`SELECT * FROM anomalies WHERE id = ?`).get(anomalyId);
100
+ if (!anomaly)
101
+ return null;
102
+ const a = this.rowToAnomaly(anomaly);
103
+ // Get metric history around the anomaly
104
+ const window = 86_400_000; // 24h
105
+ const history = this.db.prepare(`
106
+ SELECT value, timestamp FROM metric_history
107
+ WHERE metric = ? AND timestamp BETWEEN ? AND ?
108
+ ORDER BY timestamp
109
+ `).all(a.metric, a.timestamp - window, a.timestamp + window);
110
+ // Look for related events in causal_events (if available)
111
+ let relatedEvents = [];
112
+ try {
113
+ relatedEvents = this.db.prepare(`
114
+ SELECT type, source, timestamp FROM causal_events
115
+ WHERE timestamp BETWEEN ? AND ?
116
+ ORDER BY timestamp DESC LIMIT 20
117
+ `).all(a.timestamp - 3_600_000, a.timestamp);
118
+ }
119
+ catch { /* table might not exist */ }
120
+ return {
121
+ anomaly: a,
122
+ metric_history: history,
123
+ related_events: relatedEvents,
124
+ context: {
125
+ metric_count: history.length,
126
+ avg_value: history.length > 0 ? history.reduce((s, h) => s + h.value, 0) / history.length : 0,
127
+ max_value: history.length > 0 ? Math.max(...history.map(h => h.value)) : 0,
128
+ min_value: history.length > 0 ? Math.min(...history.map(h => h.value)) : 0,
129
+ },
130
+ };
131
+ }
132
+ /** Get anomaly history (including resolved). */
133
+ getHistory(limit = 50) {
134
+ return this.db.prepare(`
135
+ SELECT * FROM anomalies ORDER BY timestamp DESC LIMIT ?
136
+ `).all(limit).map(r => this.rowToAnomaly(r));
137
+ }
138
+ /** Get drift reports for all tracked metrics. */
139
+ getDriftReport() {
140
+ const reports = [];
141
+ const metrics = this.db.prepare(`
142
+ SELECT DISTINCT metric FROM metric_history
143
+ `).all();
144
+ for (const { metric } of metrics) {
145
+ const values = this.db.prepare(`
146
+ SELECT value, timestamp FROM metric_history
147
+ WHERE metric = ? ORDER BY timestamp ASC
148
+ `).all(metric);
149
+ if (values.length < this.config.minDriftPoints)
150
+ continue;
151
+ const report = this.computeDriftReport(metric, values);
152
+ if (report)
153
+ reports.push(report);
154
+ }
155
+ return reports.sort((a, b) => Math.abs(b.rate_per_day) - Math.abs(a.rate_per_day));
156
+ }
157
+ /** Resolve an anomaly. */
158
+ resolve(anomalyId, resolution) {
159
+ const result = this.db.prepare(`
160
+ UPDATE anomalies SET resolved = 1, resolution = ? WHERE id = ?
161
+ `).run(resolution, anomalyId);
162
+ return result.changes > 0;
163
+ }
164
+ detectStatisticalAnomaly(metric, values) {
165
+ if (values.length < 5)
166
+ return null;
167
+ const nums = values.map(v => v.value);
168
+ const m = mean(nums);
169
+ const s = stddev(nums);
170
+ if (s === 0)
171
+ return null;
172
+ // Check latest value
173
+ const latest = values[values.length - 1];
174
+ const z = Math.abs(latest.value - m) / s;
175
+ if (z < this.config.zThreshold)
176
+ return null;
177
+ const severity = z > 4 ? 'critical' :
178
+ z > 3 ? 'high' :
179
+ z > 2.5 ? 'medium' : 'low';
180
+ return {
181
+ timestamp: latest.timestamp,
182
+ type: 'statistical',
183
+ severity,
184
+ title: `${metric} deviated ${z.toFixed(1)}σ from mean`,
185
+ description: `${metric} = ${latest.value.toFixed(3)} (mean: ${m.toFixed(3)}, σ: ${s.toFixed(3)}, z-score: ${z.toFixed(2)})`,
186
+ metric,
187
+ expected_value: m,
188
+ actual_value: latest.value,
189
+ deviation: z,
190
+ evidence: { mean: m, stddev: s, z_score: z, n: values.length },
191
+ resolved: false,
192
+ };
193
+ }
194
+ detectDrift(metric, values) {
195
+ if (values.length < this.config.minDriftPoints)
196
+ return null;
197
+ // EWMA on the values
198
+ const alpha = this.config.ewmaAlpha;
199
+ let ewma = values[0].value;
200
+ const ewmaValues = [ewma];
201
+ for (let i = 1; i < values.length; i++) {
202
+ ewma = alpha * values[i].value + (1 - alpha) * ewma;
203
+ ewmaValues.push(ewma);
204
+ }
205
+ // Check if EWMA trend is consistently increasing or decreasing
206
+ const recentWindow = Math.min(values.length, 10);
207
+ const recentEwma = ewmaValues.slice(-recentWindow);
208
+ let increasing = 0, decreasing = 0;
209
+ for (let i = 1; i < recentEwma.length; i++) {
210
+ if (recentEwma[i] > recentEwma[i - 1])
211
+ increasing++;
212
+ else if (recentEwma[i] < recentEwma[i - 1])
213
+ decreasing++;
214
+ }
215
+ const total = recentEwma.length - 1;
216
+ if (total < 3)
217
+ return null;
218
+ const driftRatio = Math.max(increasing, decreasing) / total;
219
+ if (driftRatio < 0.7)
220
+ return null; // Not a consistent trend
221
+ const direction = increasing > decreasing ? 'increasing' : 'decreasing';
222
+ const firstValue = recentEwma[0];
223
+ const lastValue = recentEwma[recentEwma.length - 1];
224
+ const change = lastValue - firstValue;
225
+ const pctChange = firstValue !== 0 ? (change / Math.abs(firstValue)) * 100 : 0;
226
+ if (Math.abs(pctChange) < 5)
227
+ return null; // Less than 5% drift — ignore
228
+ const severity = Math.abs(pctChange) > 30 ? 'high' :
229
+ Math.abs(pctChange) > 15 ? 'medium' : 'low';
230
+ return {
231
+ timestamp: Date.now(),
232
+ type: 'drift',
233
+ severity,
234
+ title: `${metric} is ${direction} (${pctChange > 0 ? '+' : ''}${pctChange.toFixed(1)}% drift)`,
235
+ description: `${metric} has been consistently ${direction} over the last ${recentWindow} measurements. EWMA: ${firstValue.toFixed(3)} → ${lastValue.toFixed(3)}.`,
236
+ metric,
237
+ expected_value: firstValue,
238
+ actual_value: lastValue,
239
+ deviation: Math.abs(pctChange),
240
+ evidence: { direction, change, pct_change: pctChange, window: recentWindow, drift_ratio: driftRatio },
241
+ resolved: false,
242
+ };
243
+ }
244
+ computeDriftReport(metric, values) {
245
+ if (values.length < 2)
246
+ return null;
247
+ const first = values[0];
248
+ const last = values[values.length - 1];
249
+ const periodMs = last.timestamp - first.timestamp;
250
+ const periodDays = periodMs / 86_400_000;
251
+ if (periodDays < 1)
252
+ return null;
253
+ const change = last.value - first.value;
254
+ const ratePerDay = change / periodDays;
255
+ // Simple linear regression significance
256
+ const xMean = values.reduce((s, v) => s + v.timestamp, 0) / values.length;
257
+ const yMean = values.reduce((s, v) => s + v.value, 0) / values.length;
258
+ let ssXY = 0, ssXX = 0, ssYY = 0;
259
+ for (const v of values) {
260
+ ssXY += (v.timestamp - xMean) * (v.value - yMean);
261
+ ssXX += (v.timestamp - xMean) ** 2;
262
+ ssYY += (v.value - yMean) ** 2;
263
+ }
264
+ const r = ssXX > 0 && ssYY > 0 ? ssXY / Math.sqrt(ssXX * ssYY) : 0;
265
+ const significant = Math.abs(r) > 0.5 && values.length >= 5;
266
+ const direction = Math.abs(ratePerDay) < 0.001 ? 'stable' :
267
+ ratePerDay > 0 ? 'increasing' : 'decreasing';
268
+ return {
269
+ metric,
270
+ direction,
271
+ rate_per_day: ratePerDay,
272
+ cumulative_change: change,
273
+ period_days: periodDays,
274
+ significant,
275
+ description: `${metric}: ${direction} at ${ratePerDay.toFixed(4)}/day over ${periodDays.toFixed(1)} days (r=${r.toFixed(2)})`,
276
+ };
277
+ }
278
+ persistAnomaly(a) {
279
+ // Avoid duplicates: same metric + type within 1 hour
280
+ const existing = this.db.prepare(`
281
+ SELECT id FROM anomalies WHERE metric = ? AND type = ? AND timestamp > ? LIMIT 1
282
+ `).get(a.metric, a.type, Date.now() - 3_600_000);
283
+ if (existing)
284
+ return;
285
+ this.db.prepare(`
286
+ INSERT INTO anomalies (timestamp, type, severity, title, description, metric,
287
+ expected_value, actual_value, deviation, evidence)
288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
289
+ `).run(a.timestamp, a.type, a.severity, a.title, a.description, a.metric, a.expected_value, a.actual_value, a.deviation, JSON.stringify(a.evidence));
290
+ }
291
+ rowToAnomaly(row) {
292
+ return {
293
+ id: row.id,
294
+ timestamp: row.timestamp,
295
+ type: row.type,
296
+ severity: row.severity,
297
+ title: row.title,
298
+ description: row.description,
299
+ metric: row.metric,
300
+ expected_value: row.expected_value,
301
+ actual_value: row.actual_value,
302
+ deviation: row.deviation,
303
+ evidence: JSON.parse(row.evidence),
304
+ resolved: row.resolved === 1,
305
+ resolution: row.resolution,
306
+ };
307
+ }
308
+ }
309
+ function mean(arr) {
310
+ return arr.length === 0 ? 0 : arr.reduce((a, b) => a + b, 0) / arr.length;
311
+ }
312
+ function stddev(arr) {
313
+ if (arr.length < 2)
314
+ return 0;
315
+ const m = mean(arr);
316
+ return Math.sqrt(arr.reduce((s, x) => s + (x - m) ** 2, 0) / (arr.length - 1));
317
+ }
318
+ //# sourceMappingURL=anomaly-detective.js.map