chainlesschain 0.47.9 → 0.49.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 (70) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/codegen.js +303 -0
  10. package/src/commands/collab.js +482 -0
  11. package/src/commands/crosschain.js +382 -0
  12. package/src/commands/dbevo.js +388 -0
  13. package/src/commands/dev.js +411 -0
  14. package/src/commands/federation.js +427 -0
  15. package/src/commands/fusion.js +332 -0
  16. package/src/commands/governance.js +505 -0
  17. package/src/commands/hardening.js +110 -0
  18. package/src/commands/incentive.js +373 -0
  19. package/src/commands/inference.js +304 -0
  20. package/src/commands/infra.js +361 -0
  21. package/src/commands/kg.js +371 -0
  22. package/src/commands/marketplace.js +326 -0
  23. package/src/commands/mcp.js +97 -18
  24. package/src/commands/nlprog.js +329 -0
  25. package/src/commands/ops.js +408 -0
  26. package/src/commands/perception.js +385 -0
  27. package/src/commands/pqc.js +34 -0
  28. package/src/commands/privacy.js +345 -0
  29. package/src/commands/quantization.js +280 -0
  30. package/src/commands/recommend.js +336 -0
  31. package/src/commands/reputation.js +349 -0
  32. package/src/commands/runtime.js +500 -0
  33. package/src/commands/sla.js +352 -0
  34. package/src/commands/stress.js +252 -0
  35. package/src/commands/tech.js +268 -0
  36. package/src/commands/tenant.js +576 -0
  37. package/src/commands/trust.js +366 -0
  38. package/src/harness/mcp-client.js +330 -54
  39. package/src/index.js +112 -0
  40. package/src/lib/aiops.js +523 -0
  41. package/src/lib/autonomous-developer.js +524 -0
  42. package/src/lib/code-agent.js +442 -0
  43. package/src/lib/collaboration-governance.js +556 -0
  44. package/src/lib/community-governance.js +649 -0
  45. package/src/lib/content-recommendation.js +600 -0
  46. package/src/lib/cross-chain.js +669 -0
  47. package/src/lib/dbevo.js +669 -0
  48. package/src/lib/decentral-infra.js +445 -0
  49. package/src/lib/federation-hardening.js +587 -0
  50. package/src/lib/hardening-manager.js +409 -0
  51. package/src/lib/inference-network.js +407 -0
  52. package/src/lib/knowledge-graph.js +530 -0
  53. package/src/lib/mcp-client.js +3 -0
  54. package/src/lib/multimodal.js +698 -0
  55. package/src/lib/nl-programming.js +595 -0
  56. package/src/lib/perception.js +500 -0
  57. package/src/lib/pqc-manager.js +141 -9
  58. package/src/lib/privacy-computing.js +575 -0
  59. package/src/lib/protocol-fusion.js +535 -0
  60. package/src/lib/quantization.js +362 -0
  61. package/src/lib/reputation-optimizer.js +509 -0
  62. package/src/lib/skill-marketplace.js +397 -0
  63. package/src/lib/sla-manager.js +484 -0
  64. package/src/lib/stress-tester.js +383 -0
  65. package/src/lib/tech-learning-engine.js +651 -0
  66. package/src/lib/tenant-saas.js +831 -0
  67. package/src/lib/token-incentive.js +513 -0
  68. package/src/lib/trust-security.js +473 -0
  69. package/src/lib/universal-runtime.js +771 -0
  70. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Federation Hardening — CLI port of Phase 58
3
+ * (docs/design/modules/30_联邦强化系统.md).
4
+ *
5
+ * Desktop uses real Cowork federation with live WebRTC/libp2p nodes,
6
+ * real-time heartbeat monitoring, and connection pool management.
7
+ * CLI port ships:
8
+ *
9
+ * - Circuit breaker state machine (closed → open → half_open)
10
+ * - Health check recording and node health status tracking
11
+ * - Connection pool simulation with stats
12
+ * - Node registration and lifecycle management
13
+ *
14
+ * What does NOT port: real WebRTC/libp2p connections, live heartbeat
15
+ * monitoring, actual connection pooling, periodic health check timers.
16
+ */
17
+
18
+ import crypto from "crypto";
19
+
20
+ /* ── Constants ──────────────────────────────────────────── */
21
+
22
+ export const CIRCUIT_STATE = Object.freeze({
23
+ CLOSED: "closed",
24
+ OPEN: "open",
25
+ HALF_OPEN: "half_open",
26
+ });
27
+
28
+ export const HEALTH_STATUS = Object.freeze({
29
+ HEALTHY: "healthy",
30
+ DEGRADED: "degraded",
31
+ UNHEALTHY: "unhealthy",
32
+ UNKNOWN: "unknown",
33
+ });
34
+
35
+ export const HEALTH_METRIC = Object.freeze({
36
+ HEARTBEAT: "heartbeat",
37
+ LATENCY: "latency",
38
+ SUCCESS_RATE: "success_rate",
39
+ CPU_USAGE: "cpu_usage",
40
+ MEMORY_USAGE: "memory_usage",
41
+ });
42
+
43
+ export const POOL_DEFAULTS = Object.freeze({
44
+ MIN_CONNECTIONS: 5,
45
+ MAX_CONNECTIONS: 50,
46
+ IDLE_TIMEOUT: 300000,
47
+ ACQUIRE_TIMEOUT: 30000,
48
+ });
49
+
50
+ /* ── State ──────────────────────────────────────────────── */
51
+
52
+ let _breakers = new Map();
53
+ let _healthChecks = new Map();
54
+ let _pools = new Map(); // in-memory only, no DB table
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 ensureFederationHardeningTables(db) {
75
+ db.exec(`CREATE TABLE IF NOT EXISTS federation_circuit_breakers (
76
+ node_id TEXT PRIMARY KEY,
77
+ state TEXT NOT NULL DEFAULT 'closed',
78
+ failure_count INTEGER DEFAULT 0,
79
+ success_count INTEGER DEFAULT 0,
80
+ last_failure_time INTEGER,
81
+ last_success_time INTEGER,
82
+ failure_threshold INTEGER DEFAULT 5,
83
+ success_threshold INTEGER DEFAULT 2,
84
+ open_timeout INTEGER DEFAULT 60000,
85
+ state_changed_at INTEGER NOT NULL,
86
+ metadata TEXT,
87
+ created_at INTEGER NOT NULL,
88
+ updated_at INTEGER NOT NULL
89
+ )`);
90
+
91
+ db.exec(`CREATE TABLE IF NOT EXISTS federation_health_checks (
92
+ check_id TEXT PRIMARY KEY,
93
+ node_id TEXT NOT NULL,
94
+ check_type TEXT NOT NULL,
95
+ status TEXT NOT NULL,
96
+ metrics TEXT,
97
+ checked_at INTEGER NOT NULL
98
+ )`);
99
+
100
+ _loadAll(db);
101
+ }
102
+
103
+ function _loadAll(db) {
104
+ _breakers.clear();
105
+ _healthChecks.clear();
106
+ _pools.clear();
107
+
108
+ const tables = [
109
+ ["federation_circuit_breakers", _breakers, "node_id"],
110
+ ["federation_health_checks", _healthChecks, "check_id"],
111
+ ];
112
+ for (const [table, map, key] of tables) {
113
+ try {
114
+ for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
115
+ const r = _strip(row);
116
+ map.set(r[key], r);
117
+ }
118
+ } catch (_e) {
119
+ /* table may not exist */
120
+ }
121
+ }
122
+ }
123
+
124
+ /* ── Circuit Breaker ────────────────────────────────────── */
125
+
126
+ const VALID_CIRCUIT_STATES = new Set(Object.values(CIRCUIT_STATE));
127
+
128
+ export function registerNode(db, nodeId, opts = {}) {
129
+ if (!nodeId) return { registered: false, reason: "missing_node_id" };
130
+ if (_breakers.has(nodeId))
131
+ return { registered: false, reason: "already_exists" };
132
+
133
+ const now = _now();
134
+ const entry = {
135
+ node_id: nodeId,
136
+ state: "closed",
137
+ failure_count: 0,
138
+ success_count: 0,
139
+ last_failure_time: null,
140
+ last_success_time: null,
141
+ failure_threshold: opts.failureThreshold ?? 5,
142
+ success_threshold: opts.successThreshold ?? 2,
143
+ open_timeout: opts.openTimeout ?? 60000,
144
+ state_changed_at: now,
145
+ metadata: opts.metadata || null,
146
+ created_at: now,
147
+ updated_at: now,
148
+ };
149
+
150
+ db.prepare(
151
+ `INSERT INTO federation_circuit_breakers
152
+ (node_id, state, failure_count, success_count, last_failure_time, last_success_time,
153
+ failure_threshold, success_threshold, open_timeout, state_changed_at, metadata, created_at, updated_at)
154
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
155
+ ).run(
156
+ nodeId,
157
+ entry.state,
158
+ 0,
159
+ 0,
160
+ null,
161
+ null,
162
+ entry.failure_threshold,
163
+ entry.success_threshold,
164
+ entry.open_timeout,
165
+ now,
166
+ entry.metadata,
167
+ now,
168
+ now,
169
+ );
170
+
171
+ _breakers.set(nodeId, entry);
172
+ return { registered: true, nodeId };
173
+ }
174
+
175
+ export function getCircuitBreaker(db, nodeId) {
176
+ const b = _breakers.get(nodeId);
177
+ return b ? { ...b } : null;
178
+ }
179
+
180
+ export function listCircuitBreakers(db, { state, limit = 50 } = {}) {
181
+ let breakers = [..._breakers.values()];
182
+ if (state) breakers = breakers.filter((b) => b.state === state);
183
+ return breakers
184
+ .sort((a, b) => b.updated_at - a.updated_at)
185
+ .slice(0, limit)
186
+ .map((b) => ({ ...b }));
187
+ }
188
+
189
+ export function recordFailure(db, nodeId) {
190
+ const b = _breakers.get(nodeId);
191
+ if (!b) return { updated: false, reason: "not_found" };
192
+
193
+ const now = _now();
194
+ b.failure_count += 1;
195
+ b.last_failure_time = now;
196
+ b.updated_at = now;
197
+
198
+ // Check if we should trip the breaker
199
+ const oldState = b.state;
200
+ if (b.state === "closed" && b.failure_count >= b.failure_threshold) {
201
+ b.state = "open";
202
+ b.state_changed_at = now;
203
+ b.success_count = 0;
204
+ } else if (b.state === "half_open") {
205
+ // Probe failed — go back to open
206
+ b.state = "open";
207
+ b.state_changed_at = now;
208
+ b.success_count = 0;
209
+ }
210
+
211
+ db.prepare(
212
+ `UPDATE federation_circuit_breakers
213
+ SET state = ?, failure_count = ?, success_count = ?, last_failure_time = ?,
214
+ state_changed_at = ?, updated_at = ?
215
+ WHERE node_id = ?`,
216
+ ).run(
217
+ b.state,
218
+ b.failure_count,
219
+ b.success_count,
220
+ b.last_failure_time,
221
+ b.state_changed_at,
222
+ now,
223
+ nodeId,
224
+ );
225
+
226
+ return {
227
+ updated: true,
228
+ state: b.state,
229
+ previousState: oldState,
230
+ failureCount: b.failure_count,
231
+ };
232
+ }
233
+
234
+ export function recordSuccess(db, nodeId) {
235
+ const b = _breakers.get(nodeId);
236
+ if (!b) return { updated: false, reason: "not_found" };
237
+
238
+ const now = _now();
239
+ b.success_count += 1;
240
+ b.last_success_time = now;
241
+ b.updated_at = now;
242
+
243
+ const oldState = b.state;
244
+ if (b.state === "half_open" && b.success_count >= b.success_threshold) {
245
+ // Enough successful probes — close the breaker
246
+ b.state = "closed";
247
+ b.state_changed_at = now;
248
+ b.failure_count = 0;
249
+ }
250
+
251
+ db.prepare(
252
+ `UPDATE federation_circuit_breakers
253
+ SET state = ?, failure_count = ?, success_count = ?, last_success_time = ?,
254
+ state_changed_at = ?, updated_at = ?
255
+ WHERE node_id = ?`,
256
+ ).run(
257
+ b.state,
258
+ b.failure_count,
259
+ b.success_count,
260
+ b.last_success_time,
261
+ b.state_changed_at,
262
+ now,
263
+ nodeId,
264
+ );
265
+
266
+ return {
267
+ updated: true,
268
+ state: b.state,
269
+ previousState: oldState,
270
+ successCount: b.success_count,
271
+ };
272
+ }
273
+
274
+ export function tryHalfOpen(db, nodeId) {
275
+ const b = _breakers.get(nodeId);
276
+ if (!b) return { updated: false, reason: "not_found" };
277
+ if (b.state !== "open")
278
+ return { updated: false, reason: "not_open", state: b.state };
279
+
280
+ const now = _now();
281
+ const elapsed = now - b.state_changed_at;
282
+ if (elapsed < b.open_timeout) {
283
+ return {
284
+ updated: false,
285
+ reason: "timeout_not_elapsed",
286
+ remainingMs: b.open_timeout - elapsed,
287
+ };
288
+ }
289
+
290
+ b.state = "half_open";
291
+ b.success_count = 0;
292
+ b.state_changed_at = now;
293
+ b.updated_at = now;
294
+
295
+ db.prepare(
296
+ `UPDATE federation_circuit_breakers
297
+ SET state = ?, success_count = ?, state_changed_at = ?, updated_at = ?
298
+ WHERE node_id = ?`,
299
+ ).run("half_open", 0, now, now, nodeId);
300
+
301
+ return { updated: true, state: "half_open" };
302
+ }
303
+
304
+ export function resetCircuitBreaker(db, nodeId) {
305
+ const b = _breakers.get(nodeId);
306
+ if (!b) return { reset: false, reason: "not_found" };
307
+
308
+ const now = _now();
309
+ b.state = "closed";
310
+ b.failure_count = 0;
311
+ b.success_count = 0;
312
+ b.state_changed_at = now;
313
+ b.updated_at = now;
314
+
315
+ db.prepare(
316
+ `UPDATE federation_circuit_breakers
317
+ SET state = 'closed', failure_count = 0, success_count = 0,
318
+ state_changed_at = ?, updated_at = ?
319
+ WHERE node_id = ?`,
320
+ ).run(now, now, nodeId);
321
+
322
+ return { reset: true, state: "closed" };
323
+ }
324
+
325
+ export function removeNode(db, nodeId) {
326
+ const b = _breakers.get(nodeId);
327
+ if (!b) return { removed: false, reason: "not_found" };
328
+
329
+ _breakers.delete(nodeId);
330
+ db.prepare("DELETE FROM federation_circuit_breakers WHERE node_id = ?").run(
331
+ nodeId,
332
+ );
333
+
334
+ // Also remove health checks for this node
335
+ const checksToRemove = [..._healthChecks.values()].filter(
336
+ (c) => c.node_id === nodeId,
337
+ );
338
+ for (const c of checksToRemove) {
339
+ _healthChecks.delete(c.check_id);
340
+ }
341
+ db.prepare("DELETE FROM federation_health_checks WHERE node_id = ?").run(
342
+ nodeId,
343
+ );
344
+
345
+ // Remove pool
346
+ _pools.delete(nodeId);
347
+
348
+ return { removed: true };
349
+ }
350
+
351
+ /* ── Health Check ───────────────────────────────────────── */
352
+
353
+ const VALID_CHECK_TYPES = new Set(Object.values(HEALTH_METRIC));
354
+ const VALID_HEALTH_STATUSES = new Set(Object.values(HEALTH_STATUS));
355
+
356
+ export function recordHealthCheck(
357
+ db,
358
+ { nodeId, checkType, status, metrics } = {},
359
+ ) {
360
+ if (!nodeId) return { recorded: false, reason: "missing_node_id" };
361
+ if (!checkType || !VALID_CHECK_TYPES.has(checkType))
362
+ return { recorded: false, reason: "invalid_check_type" };
363
+ if (!status || !VALID_HEALTH_STATUSES.has(status))
364
+ return { recorded: false, reason: "invalid_status" };
365
+
366
+ const checkId = _id();
367
+ const now = _now();
368
+ const metricsJson = metrics
369
+ ? typeof metrics === "string"
370
+ ? metrics
371
+ : JSON.stringify(metrics)
372
+ : null;
373
+
374
+ const entry = {
375
+ check_id: checkId,
376
+ node_id: nodeId,
377
+ check_type: checkType,
378
+ status,
379
+ metrics: metricsJson,
380
+ checked_at: now,
381
+ };
382
+
383
+ db.prepare(
384
+ `INSERT INTO federation_health_checks (check_id, node_id, check_type, status, metrics, checked_at)
385
+ VALUES (?, ?, ?, ?, ?, ?)`,
386
+ ).run(checkId, nodeId, checkType, status, metricsJson, now);
387
+
388
+ _healthChecks.set(checkId, entry);
389
+ return { recorded: true, checkId };
390
+ }
391
+
392
+ export function getHealthCheck(db, checkId) {
393
+ const c = _healthChecks.get(checkId);
394
+ return c ? { ...c } : null;
395
+ }
396
+
397
+ export function listHealthChecks(
398
+ db,
399
+ { nodeId, checkType, status, limit = 50 } = {},
400
+ ) {
401
+ let checks = [..._healthChecks.values()];
402
+ if (nodeId) checks = checks.filter((c) => c.node_id === nodeId);
403
+ if (checkType) checks = checks.filter((c) => c.check_type === checkType);
404
+ if (status) checks = checks.filter((c) => c.status === status);
405
+ return checks
406
+ .sort((a, b) => b.checked_at - a.checked_at)
407
+ .slice(0, limit)
408
+ .map((c) => ({ ...c }));
409
+ }
410
+
411
+ export function getNodeHealth(db, nodeId) {
412
+ const checks = [..._healthChecks.values()]
413
+ .filter((c) => c.node_id === nodeId)
414
+ .sort((a, b) => b.checked_at - a.checked_at);
415
+
416
+ if (checks.length === 0) return { nodeId, status: "unknown", checks: 0 };
417
+
418
+ // Aggregate: use the most recent check's status per check_type
419
+ const latestByType = new Map();
420
+ for (const c of checks) {
421
+ if (!latestByType.has(c.check_type)) {
422
+ latestByType.set(c.check_type, c);
423
+ }
424
+ }
425
+
426
+ const statuses = [...latestByType.values()].map((c) => c.status);
427
+
428
+ // Overall status: worst-case
429
+ let overall = "healthy";
430
+ if (statuses.includes("unhealthy")) overall = "unhealthy";
431
+ else if (statuses.includes("degraded")) overall = "degraded";
432
+ else if (statuses.includes("unknown")) overall = "unknown";
433
+
434
+ return {
435
+ nodeId,
436
+ status: overall,
437
+ checks: checks.length,
438
+ latestChecks: [...latestByType.values()].map((c) => ({
439
+ checkType: c.check_type,
440
+ status: c.status,
441
+ checkedAt: c.checked_at,
442
+ })),
443
+ };
444
+ }
445
+
446
+ /* ── Connection Pool (in-memory simulation) ─────────────── */
447
+
448
+ export function initPool(nodeId, opts = {}) {
449
+ if (!nodeId) return { initialized: false, reason: "missing_node_id" };
450
+ if (_pools.has(nodeId))
451
+ return { initialized: false, reason: "already_exists" };
452
+
453
+ const pool = {
454
+ nodeId,
455
+ minConnections: opts.minConnections || POOL_DEFAULTS.MIN_CONNECTIONS,
456
+ maxConnections: opts.maxConnections || POOL_DEFAULTS.MAX_CONNECTIONS,
457
+ idleTimeout: opts.idleTimeout || POOL_DEFAULTS.IDLE_TIMEOUT,
458
+ activeConnections: 0,
459
+ idleConnections: opts.minConnections || POOL_DEFAULTS.MIN_CONNECTIONS,
460
+ totalCreated: opts.minConnections || POOL_DEFAULTS.MIN_CONNECTIONS,
461
+ totalDestroyed: 0,
462
+ waitingRequests: 0,
463
+ createdAt: _now(),
464
+ };
465
+
466
+ _pools.set(nodeId, pool);
467
+ return { initialized: true, nodeId };
468
+ }
469
+
470
+ export function acquireConnection(nodeId) {
471
+ const pool = _pools.get(nodeId);
472
+ if (!pool) return { acquired: false, reason: "pool_not_found" };
473
+
474
+ if (pool.idleConnections > 0) {
475
+ pool.idleConnections -= 1;
476
+ pool.activeConnections += 1;
477
+ return {
478
+ acquired: true,
479
+ active: pool.activeConnections,
480
+ idle: pool.idleConnections,
481
+ };
482
+ }
483
+
484
+ if (pool.activeConnections < pool.maxConnections) {
485
+ pool.activeConnections += 1;
486
+ pool.totalCreated += 1;
487
+ return {
488
+ acquired: true,
489
+ active: pool.activeConnections,
490
+ idle: pool.idleConnections,
491
+ };
492
+ }
493
+
494
+ pool.waitingRequests += 1;
495
+ return {
496
+ acquired: false,
497
+ reason: "pool_exhausted",
498
+ waiting: pool.waitingRequests,
499
+ };
500
+ }
501
+
502
+ export function releaseConnection(nodeId) {
503
+ const pool = _pools.get(nodeId);
504
+ if (!pool) return { released: false, reason: "pool_not_found" };
505
+ if (pool.activeConnections <= 0)
506
+ return { released: false, reason: "no_active_connections" };
507
+
508
+ pool.activeConnections -= 1;
509
+ pool.idleConnections += 1;
510
+
511
+ // Serve waiting request if any
512
+ if (pool.waitingRequests > 0) {
513
+ pool.waitingRequests -= 1;
514
+ pool.idleConnections -= 1;
515
+ pool.activeConnections += 1;
516
+ }
517
+
518
+ return {
519
+ released: true,
520
+ active: pool.activeConnections,
521
+ idle: pool.idleConnections,
522
+ };
523
+ }
524
+
525
+ export function getPoolStats(nodeId) {
526
+ const pool = _pools.get(nodeId);
527
+ if (!pool) return null;
528
+ return { ...pool };
529
+ }
530
+
531
+ export function listPools() {
532
+ return [..._pools.values()].map((p) => ({ ...p }));
533
+ }
534
+
535
+ export function destroyPool(nodeId) {
536
+ const pool = _pools.get(nodeId);
537
+ if (!pool) return { destroyed: false, reason: "pool_not_found" };
538
+
539
+ pool.totalDestroyed += pool.activeConnections + pool.idleConnections;
540
+ _pools.delete(nodeId);
541
+ return { destroyed: true };
542
+ }
543
+
544
+ /* ── Stats ──────────────────────────────────────────────── */
545
+
546
+ export function getFederationHardeningStats(db) {
547
+ const breakers = [..._breakers.values()];
548
+ const checks = [..._healthChecks.values()];
549
+ const pools = [..._pools.values()];
550
+
551
+ const byState = {};
552
+ for (const s of Object.values(CIRCUIT_STATE)) byState[s] = 0;
553
+ for (const b of breakers) byState[b.state] = (byState[b.state] || 0) + 1;
554
+
555
+ const byHealthStatus = {};
556
+ for (const s of Object.values(HEALTH_STATUS)) byHealthStatus[s] = 0;
557
+ for (const c of checks)
558
+ byHealthStatus[c.status] = (byHealthStatus[c.status] || 0) + 1;
559
+
560
+ const totalActive = pools.reduce((s, p) => s + p.activeConnections, 0);
561
+ const totalIdle = pools.reduce((s, p) => s + p.idleConnections, 0);
562
+
563
+ return {
564
+ circuitBreakers: {
565
+ total: breakers.length,
566
+ byState,
567
+ },
568
+ healthChecks: {
569
+ total: checks.length,
570
+ byStatus: byHealthStatus,
571
+ },
572
+ connectionPools: {
573
+ total: pools.length,
574
+ totalActive,
575
+ totalIdle,
576
+ totalConnections: totalActive + totalIdle,
577
+ },
578
+ };
579
+ }
580
+
581
+ /* ── Reset (tests) ──────────────────────────────────────── */
582
+
583
+ export function _resetState() {
584
+ _breakers.clear();
585
+ _healthChecks.clear();
586
+ _pools.clear();
587
+ }