chainlesschain 0.47.9 → 0.51.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/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/multimodal.js +404 -0
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +118 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +725 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
package/src/lib/aiops.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous Ops (AIOps) — CLI port of 25_自治运维系统
|
|
3
|
+
*
|
|
4
|
+
* Desktop uses real-time anomaly detection, auto-remediation playbooks,
|
|
5
|
+
* rollback orchestration, alert escalation chains, and postmortem generation.
|
|
6
|
+
* CLI port ships:
|
|
7
|
+
*
|
|
8
|
+
* - Anomaly detection (Z-Score / IQR heuristics on metric baselines)
|
|
9
|
+
* - Incident lifecycle (open → acknowledged → resolved → closed)
|
|
10
|
+
* - Remediation playbook CRUD (trigger conditions, step lists)
|
|
11
|
+
* - Metrics baseline tracking (mean/stddev/IQR per metric)
|
|
12
|
+
* - Simulated postmortem generation
|
|
13
|
+
*
|
|
14
|
+
* What does NOT port: real-time metric streaming, Docker/Git rollback,
|
|
15
|
+
* alert escalation chains, webhook/email/IM notification channels,
|
|
16
|
+
* EWMA algorithm, post-deploy health monitoring.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import crypto from "crypto";
|
|
20
|
+
|
|
21
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
export const SEVERITY = Object.freeze({
|
|
24
|
+
P0: "P0",
|
|
25
|
+
P1: "P1",
|
|
26
|
+
P2: "P2",
|
|
27
|
+
P3: "P3",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const INCIDENT_STATUS = Object.freeze({
|
|
31
|
+
OPEN: "open",
|
|
32
|
+
ACKNOWLEDGED: "acknowledged",
|
|
33
|
+
RESOLVED: "resolved",
|
|
34
|
+
CLOSED: "closed",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const DETECTION_ALGORITHM = Object.freeze({
|
|
38
|
+
Z_SCORE: "z_score",
|
|
39
|
+
IQR: "iqr",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const ROLLBACK_TYPE = Object.freeze({
|
|
43
|
+
GIT: "git",
|
|
44
|
+
DOCKER: "docker",
|
|
45
|
+
CONFIG: "config",
|
|
46
|
+
SERVICE: "service",
|
|
47
|
+
CUSTOM: "custom",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
51
|
+
|
|
52
|
+
let _incidents = new Map();
|
|
53
|
+
let _playbooks = new Map();
|
|
54
|
+
let _baselines = new Map();
|
|
55
|
+
|
|
56
|
+
function _id() {
|
|
57
|
+
return crypto.randomUUID();
|
|
58
|
+
}
|
|
59
|
+
function _now() {
|
|
60
|
+
return Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _strip(row) {
|
|
64
|
+
if (!row) return null;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [k, v] of Object.entries(row)) {
|
|
67
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
export function ensureAiOpsTables(db) {
|
|
75
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ops_incidents (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
anomaly_metric TEXT,
|
|
78
|
+
severity TEXT DEFAULT 'P3',
|
|
79
|
+
status TEXT DEFAULT 'open',
|
|
80
|
+
description TEXT,
|
|
81
|
+
anomaly_data TEXT,
|
|
82
|
+
remediation_id TEXT,
|
|
83
|
+
postmortem TEXT,
|
|
84
|
+
acknowledged_at INTEGER,
|
|
85
|
+
resolved_at INTEGER,
|
|
86
|
+
created_at INTEGER
|
|
87
|
+
)`);
|
|
88
|
+
|
|
89
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ops_remediation_playbooks (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
name TEXT NOT NULL,
|
|
92
|
+
trigger_condition TEXT,
|
|
93
|
+
steps TEXT,
|
|
94
|
+
enabled INTEGER DEFAULT 1,
|
|
95
|
+
success_count INTEGER DEFAULT 0,
|
|
96
|
+
failure_count INTEGER DEFAULT 0,
|
|
97
|
+
created_at INTEGER
|
|
98
|
+
)`);
|
|
99
|
+
|
|
100
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ops_metrics_baseline (
|
|
101
|
+
metric_name TEXT PRIMARY KEY,
|
|
102
|
+
mean REAL DEFAULT 0,
|
|
103
|
+
std_dev REAL DEFAULT 0,
|
|
104
|
+
q1 REAL DEFAULT 0,
|
|
105
|
+
q3 REAL DEFAULT 0,
|
|
106
|
+
sample_count INTEGER DEFAULT 0,
|
|
107
|
+
updated_at INTEGER
|
|
108
|
+
)`);
|
|
109
|
+
|
|
110
|
+
_loadAll(db);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _loadAll(db) {
|
|
114
|
+
_incidents.clear();
|
|
115
|
+
_playbooks.clear();
|
|
116
|
+
_baselines.clear();
|
|
117
|
+
|
|
118
|
+
const tables = [
|
|
119
|
+
["ops_incidents", _incidents, "id"],
|
|
120
|
+
["ops_remediation_playbooks", _playbooks, "id"],
|
|
121
|
+
["ops_metrics_baseline", _baselines, "metric_name"],
|
|
122
|
+
];
|
|
123
|
+
for (const [table, map, key] of tables) {
|
|
124
|
+
try {
|
|
125
|
+
for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
|
|
126
|
+
const r = _strip(row);
|
|
127
|
+
map.set(r[key], r);
|
|
128
|
+
}
|
|
129
|
+
} catch (_e) {
|
|
130
|
+
/* table may not exist */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ── Metrics Baseline ───────────────────────────────────── */
|
|
136
|
+
|
|
137
|
+
export function updateBaseline(db, metricName, values) {
|
|
138
|
+
if (!metricName) return { updated: false, reason: "missing_metric_name" };
|
|
139
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
140
|
+
return { updated: false, reason: "empty_values" };
|
|
141
|
+
|
|
142
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
143
|
+
const n = sorted.length;
|
|
144
|
+
const mean =
|
|
145
|
+
Math.round((sorted.reduce((s, v) => s + v, 0) / n) * 1000) / 1000;
|
|
146
|
+
const variance = sorted.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
147
|
+
const stdDev = Math.round(Math.sqrt(variance) * 1000) / 1000;
|
|
148
|
+
const q1 = sorted[Math.floor(n * 0.25)];
|
|
149
|
+
const q3 = sorted[Math.floor(n * 0.75)];
|
|
150
|
+
const now = _now();
|
|
151
|
+
|
|
152
|
+
const existing = _baselines.get(metricName);
|
|
153
|
+
if (existing) {
|
|
154
|
+
db.prepare(
|
|
155
|
+
`UPDATE ops_metrics_baseline SET mean = ?, std_dev = ?, q1 = ?, q3 = ?, sample_count = ?, updated_at = ? WHERE metric_name = ?`,
|
|
156
|
+
).run(mean, stdDev, q1, q3, n, now, metricName);
|
|
157
|
+
} else {
|
|
158
|
+
db.prepare(
|
|
159
|
+
`INSERT INTO ops_metrics_baseline (metric_name, mean, std_dev, q1, q3, sample_count, updated_at)
|
|
160
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
161
|
+
).run(metricName, mean, stdDev, q1, q3, n, now);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const baseline = {
|
|
165
|
+
metric_name: metricName,
|
|
166
|
+
mean,
|
|
167
|
+
std_dev: stdDev,
|
|
168
|
+
q1,
|
|
169
|
+
q3,
|
|
170
|
+
sample_count: n,
|
|
171
|
+
updated_at: now,
|
|
172
|
+
};
|
|
173
|
+
_baselines.set(metricName, baseline);
|
|
174
|
+
return { updated: true, baseline };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getBaseline(db, metricName) {
|
|
178
|
+
const b = _baselines.get(metricName);
|
|
179
|
+
return b ? { ...b } : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function listBaselines(db) {
|
|
183
|
+
return [..._baselines.values()].map((b) => ({ ...b }));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── Anomaly Detection ──────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
export function detectAnomaly(db, { metricName, value, algorithm } = {}) {
|
|
189
|
+
if (!metricName) return { anomaly: false, reason: "missing_metric_name" };
|
|
190
|
+
if (value == null) return { anomaly: false, reason: "missing_value" };
|
|
191
|
+
|
|
192
|
+
const baseline = _baselines.get(metricName);
|
|
193
|
+
if (!baseline) return { anomaly: false, reason: "no_baseline" };
|
|
194
|
+
|
|
195
|
+
const algo = algorithm || "z_score";
|
|
196
|
+
let isAnomaly = false;
|
|
197
|
+
let score = 0;
|
|
198
|
+
let threshold = 0;
|
|
199
|
+
|
|
200
|
+
if (algo === "z_score") {
|
|
201
|
+
if (baseline.std_dev === 0)
|
|
202
|
+
return { anomaly: false, reason: "zero_stddev" };
|
|
203
|
+
score = Math.abs(value - baseline.mean) / baseline.std_dev;
|
|
204
|
+
threshold = 3.0;
|
|
205
|
+
isAnomaly = score > threshold;
|
|
206
|
+
} else if (algo === "iqr") {
|
|
207
|
+
const iqr = baseline.q3 - baseline.q1;
|
|
208
|
+
const lower = baseline.q1 - 1.5 * iqr;
|
|
209
|
+
const upper = baseline.q3 + 1.5 * iqr;
|
|
210
|
+
score =
|
|
211
|
+
value < lower
|
|
212
|
+
? (lower - value) / (iqr || 1)
|
|
213
|
+
: value > upper
|
|
214
|
+
? (value - upper) / (iqr || 1)
|
|
215
|
+
: 0;
|
|
216
|
+
threshold = 0;
|
|
217
|
+
isAnomaly = value < lower || value > upper;
|
|
218
|
+
} else {
|
|
219
|
+
return { anomaly: false, reason: "unknown_algorithm" };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = {
|
|
223
|
+
anomaly: isAnomaly,
|
|
224
|
+
metricName,
|
|
225
|
+
value,
|
|
226
|
+
algorithm: algo,
|
|
227
|
+
score: Math.round(score * 1000) / 1000,
|
|
228
|
+
threshold,
|
|
229
|
+
baseline: {
|
|
230
|
+
mean: baseline.mean,
|
|
231
|
+
std_dev: baseline.std_dev,
|
|
232
|
+
q1: baseline.q1,
|
|
233
|
+
q3: baseline.q3,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Auto-create incident if anomaly detected
|
|
238
|
+
if (isAnomaly) {
|
|
239
|
+
const severity =
|
|
240
|
+
score > 6 ? "P0" : score > 4 ? "P1" : score > 2 ? "P2" : "P3";
|
|
241
|
+
const inc = createIncident(db, {
|
|
242
|
+
anomalyMetric: metricName,
|
|
243
|
+
severity,
|
|
244
|
+
description: `Anomaly detected: ${metricName}=${value} (${algo} score=${result.score})`,
|
|
245
|
+
anomalyData: JSON.stringify(result),
|
|
246
|
+
});
|
|
247
|
+
result.incidentId = inc.incidentId;
|
|
248
|
+
result.severity = severity;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Incidents ──────────────────────────────────────────── */
|
|
255
|
+
|
|
256
|
+
const VALID_SEVERITIES = new Set(Object.values(SEVERITY));
|
|
257
|
+
const VALID_STATUSES = new Set(Object.values(INCIDENT_STATUS));
|
|
258
|
+
|
|
259
|
+
export function createIncident(
|
|
260
|
+
db,
|
|
261
|
+
{ anomalyMetric, severity, description, anomalyData } = {},
|
|
262
|
+
) {
|
|
263
|
+
const sev = severity && VALID_SEVERITIES.has(severity) ? severity : "P3";
|
|
264
|
+
const id = _id();
|
|
265
|
+
const now = _now();
|
|
266
|
+
|
|
267
|
+
const entry = {
|
|
268
|
+
id,
|
|
269
|
+
anomaly_metric: anomalyMetric || null,
|
|
270
|
+
severity: sev,
|
|
271
|
+
status: "open",
|
|
272
|
+
description: description || null,
|
|
273
|
+
anomaly_data: anomalyData || null,
|
|
274
|
+
remediation_id: null,
|
|
275
|
+
postmortem: null,
|
|
276
|
+
acknowledged_at: null,
|
|
277
|
+
resolved_at: null,
|
|
278
|
+
created_at: now,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
db.prepare(
|
|
282
|
+
`INSERT INTO ops_incidents (id, anomaly_metric, severity, status, description, anomaly_data, remediation_id, postmortem, acknowledged_at, resolved_at, created_at)
|
|
283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
284
|
+
).run(
|
|
285
|
+
id,
|
|
286
|
+
entry.anomaly_metric,
|
|
287
|
+
sev,
|
|
288
|
+
"open",
|
|
289
|
+
entry.description,
|
|
290
|
+
entry.anomaly_data,
|
|
291
|
+
null,
|
|
292
|
+
null,
|
|
293
|
+
null,
|
|
294
|
+
null,
|
|
295
|
+
now,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
_incidents.set(id, entry);
|
|
299
|
+
return { incidentId: id };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function getIncident(db, id) {
|
|
303
|
+
const i = _incidents.get(id);
|
|
304
|
+
return i ? { ...i } : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function acknowledgeIncident(db, id) {
|
|
308
|
+
const i = _incidents.get(id);
|
|
309
|
+
if (!i) return { acknowledged: false, reason: "not_found" };
|
|
310
|
+
if (i.status !== "open") return { acknowledged: false, reason: "not_open" };
|
|
311
|
+
|
|
312
|
+
i.status = "acknowledged";
|
|
313
|
+
i.acknowledged_at = _now();
|
|
314
|
+
db.prepare(
|
|
315
|
+
"UPDATE ops_incidents SET status = ?, acknowledged_at = ? WHERE id = ?",
|
|
316
|
+
).run(i.status, i.acknowledged_at, id);
|
|
317
|
+
|
|
318
|
+
return { acknowledged: true };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function resolveIncident(db, id) {
|
|
322
|
+
const i = _incidents.get(id);
|
|
323
|
+
if (!i) return { resolved: false, reason: "not_found" };
|
|
324
|
+
if (i.status !== "open" && i.status !== "acknowledged")
|
|
325
|
+
return { resolved: false, reason: "not_resolvable" };
|
|
326
|
+
|
|
327
|
+
i.status = "resolved";
|
|
328
|
+
i.resolved_at = _now();
|
|
329
|
+
db.prepare(
|
|
330
|
+
"UPDATE ops_incidents SET status = ?, resolved_at = ? WHERE id = ?",
|
|
331
|
+
).run(i.status, i.resolved_at, id);
|
|
332
|
+
|
|
333
|
+
return { resolved: true };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function closeIncident(db, id) {
|
|
337
|
+
const i = _incidents.get(id);
|
|
338
|
+
if (!i) return { closed: false, reason: "not_found" };
|
|
339
|
+
if (i.status !== "resolved") return { closed: false, reason: "not_resolved" };
|
|
340
|
+
|
|
341
|
+
i.status = "closed";
|
|
342
|
+
db.prepare("UPDATE ops_incidents SET status = ? WHERE id = ?").run(
|
|
343
|
+
"closed",
|
|
344
|
+
id,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
return { closed: true };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function listIncidents(db, { severity, status, limit = 50 } = {}) {
|
|
351
|
+
let incs = [..._incidents.values()];
|
|
352
|
+
if (severity) incs = incs.filter((i) => i.severity === severity);
|
|
353
|
+
if (status) incs = incs.filter((i) => i.status === status);
|
|
354
|
+
return incs
|
|
355
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
356
|
+
.slice(0, limit)
|
|
357
|
+
.map((i) => ({ ...i }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ── Playbooks ──────────────────────────────────────────── */
|
|
361
|
+
|
|
362
|
+
export function createPlaybook(db, { name, triggerCondition, steps } = {}) {
|
|
363
|
+
if (!name) return { playbookId: null, reason: "missing_name" };
|
|
364
|
+
|
|
365
|
+
const id = _id();
|
|
366
|
+
const now = _now();
|
|
367
|
+
|
|
368
|
+
const entry = {
|
|
369
|
+
id,
|
|
370
|
+
name,
|
|
371
|
+
trigger_condition: triggerCondition || null,
|
|
372
|
+
steps: steps || null,
|
|
373
|
+
enabled: 1,
|
|
374
|
+
success_count: 0,
|
|
375
|
+
failure_count: 0,
|
|
376
|
+
created_at: now,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
db.prepare(
|
|
380
|
+
`INSERT INTO ops_remediation_playbooks (id, name, trigger_condition, steps, enabled, success_count, failure_count, created_at)
|
|
381
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
382
|
+
).run(id, name, entry.trigger_condition, entry.steps, 1, 0, 0, now);
|
|
383
|
+
|
|
384
|
+
_playbooks.set(id, entry);
|
|
385
|
+
return { playbookId: id };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function getPlaybook(db, id) {
|
|
389
|
+
const p = _playbooks.get(id);
|
|
390
|
+
return p ? { ...p } : null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function togglePlaybook(db, id, enabled) {
|
|
394
|
+
const p = _playbooks.get(id);
|
|
395
|
+
if (!p) return { toggled: false, reason: "not_found" };
|
|
396
|
+
|
|
397
|
+
p.enabled = enabled ? 1 : 0;
|
|
398
|
+
db.prepare(
|
|
399
|
+
"UPDATE ops_remediation_playbooks SET enabled = ? WHERE id = ?",
|
|
400
|
+
).run(p.enabled, id);
|
|
401
|
+
|
|
402
|
+
return { toggled: true, enabled: p.enabled };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function recordPlaybookResult(db, id, success) {
|
|
406
|
+
const p = _playbooks.get(id);
|
|
407
|
+
if (!p) return { recorded: false, reason: "not_found" };
|
|
408
|
+
|
|
409
|
+
if (success) p.success_count += 1;
|
|
410
|
+
else p.failure_count += 1;
|
|
411
|
+
|
|
412
|
+
db.prepare(
|
|
413
|
+
"UPDATE ops_remediation_playbooks SET success_count = ?, failure_count = ? WHERE id = ?",
|
|
414
|
+
).run(p.success_count, p.failure_count, id);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
recorded: true,
|
|
418
|
+
successCount: p.success_count,
|
|
419
|
+
failureCount: p.failure_count,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function listPlaybooks(db, { enabled, limit = 50 } = {}) {
|
|
424
|
+
let pbs = [..._playbooks.values()];
|
|
425
|
+
if (enabled != null) pbs = pbs.filter((p) => p.enabled === (enabled ? 1 : 0));
|
|
426
|
+
return pbs
|
|
427
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
428
|
+
.slice(0, limit)
|
|
429
|
+
.map((p) => ({ ...p }));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* ── Postmortem ─────────────────────────────────────────── */
|
|
433
|
+
|
|
434
|
+
export function generatePostmortem(db, id) {
|
|
435
|
+
const i = _incidents.get(id);
|
|
436
|
+
if (!i) return { generated: false, reason: "not_found" };
|
|
437
|
+
if (i.status !== "resolved" && i.status !== "closed")
|
|
438
|
+
return { generated: false, reason: "not_resolved" };
|
|
439
|
+
|
|
440
|
+
const durationMs = (i.resolved_at || _now()) - i.created_at;
|
|
441
|
+
const postmortem = {
|
|
442
|
+
incidentId: id,
|
|
443
|
+
severity: i.severity,
|
|
444
|
+
metric: i.anomaly_metric,
|
|
445
|
+
description: i.description,
|
|
446
|
+
timeline: {
|
|
447
|
+
created: i.created_at,
|
|
448
|
+
acknowledged: i.acknowledged_at,
|
|
449
|
+
resolved: i.resolved_at,
|
|
450
|
+
timeToAcknowledgeMs: i.acknowledged_at
|
|
451
|
+
? i.acknowledged_at - i.created_at
|
|
452
|
+
: null,
|
|
453
|
+
timeToResolveMs: durationMs,
|
|
454
|
+
},
|
|
455
|
+
rootCause: i.anomaly_data
|
|
456
|
+
? "Anomaly detected via automated monitoring"
|
|
457
|
+
: "Manual incident report",
|
|
458
|
+
impact: `${i.severity} incident on metric ${i.anomaly_metric || "unknown"}`,
|
|
459
|
+
remediation: i.remediation_id || "Manual resolution",
|
|
460
|
+
generatedAt: _now(),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
i.postmortem = JSON.stringify(postmortem);
|
|
464
|
+
db.prepare("UPDATE ops_incidents SET postmortem = ? WHERE id = ?").run(
|
|
465
|
+
i.postmortem,
|
|
466
|
+
id,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
return { generated: true, postmortem };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ── Stats ──────────────────────────────────────────────── */
|
|
473
|
+
|
|
474
|
+
export function getOpsStats(db) {
|
|
475
|
+
const incs = [..._incidents.values()];
|
|
476
|
+
const pbs = [..._playbooks.values()];
|
|
477
|
+
const bls = [..._baselines.values()];
|
|
478
|
+
|
|
479
|
+
const bySeverity = {};
|
|
480
|
+
for (const sev of Object.values(SEVERITY)) bySeverity[sev] = 0;
|
|
481
|
+
for (const i of incs)
|
|
482
|
+
bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1;
|
|
483
|
+
|
|
484
|
+
const byStatus = {};
|
|
485
|
+
for (const st of Object.values(INCIDENT_STATUS)) byStatus[st] = 0;
|
|
486
|
+
for (const i of incs) byStatus[i.status] = (byStatus[i.status] || 0) + 1;
|
|
487
|
+
|
|
488
|
+
const resolved = incs.filter((i) => i.resolved_at);
|
|
489
|
+
const avgResolveMs =
|
|
490
|
+
resolved.length > 0
|
|
491
|
+
? Math.round(
|
|
492
|
+
resolved.reduce((s, i) => s + (i.resolved_at - i.created_at), 0) /
|
|
493
|
+
resolved.length,
|
|
494
|
+
)
|
|
495
|
+
: 0;
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
incidents: {
|
|
499
|
+
total: incs.length,
|
|
500
|
+
bySeverity,
|
|
501
|
+
byStatus,
|
|
502
|
+
avgResolveMs,
|
|
503
|
+
},
|
|
504
|
+
playbooks: {
|
|
505
|
+
total: pbs.length,
|
|
506
|
+
enabled: pbs.filter((p) => p.enabled).length,
|
|
507
|
+
totalSuccess: pbs.reduce((s, p) => s + p.success_count, 0),
|
|
508
|
+
totalFailure: pbs.reduce((s, p) => s + p.failure_count, 0),
|
|
509
|
+
},
|
|
510
|
+
baselines: {
|
|
511
|
+
total: bls.length,
|
|
512
|
+
metrics: bls.map((b) => b.metric_name),
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
518
|
+
|
|
519
|
+
export function _resetState() {
|
|
520
|
+
_incidents.clear();
|
|
521
|
+
_playbooks.clear();
|
|
522
|
+
_baselines.clear();
|
|
523
|
+
}
|