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.
Files changed (73) 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/ipfs.js +392 -0
  22. package/src/commands/kg.js +371 -0
  23. package/src/commands/marketplace.js +326 -0
  24. package/src/commands/mcp.js +97 -18
  25. package/src/commands/multimodal.js +404 -0
  26. package/src/commands/nlprog.js +329 -0
  27. package/src/commands/ops.js +408 -0
  28. package/src/commands/perception.js +385 -0
  29. package/src/commands/pqc.js +34 -0
  30. package/src/commands/privacy.js +345 -0
  31. package/src/commands/quantization.js +280 -0
  32. package/src/commands/recommend.js +336 -0
  33. package/src/commands/reputation.js +349 -0
  34. package/src/commands/runtime.js +500 -0
  35. package/src/commands/sla.js +352 -0
  36. package/src/commands/stress.js +252 -0
  37. package/src/commands/tech.js +268 -0
  38. package/src/commands/tenant.js +576 -0
  39. package/src/commands/trust.js +366 -0
  40. package/src/harness/mcp-client.js +330 -54
  41. package/src/index.js +118 -0
  42. package/src/lib/aiops.js +523 -0
  43. package/src/lib/autonomous-developer.js +524 -0
  44. package/src/lib/code-agent.js +442 -0
  45. package/src/lib/collaboration-governance.js +556 -0
  46. package/src/lib/community-governance.js +649 -0
  47. package/src/lib/content-recommendation.js +600 -0
  48. package/src/lib/cross-chain.js +669 -0
  49. package/src/lib/dbevo.js +669 -0
  50. package/src/lib/decentral-infra.js +445 -0
  51. package/src/lib/federation-hardening.js +587 -0
  52. package/src/lib/hardening-manager.js +409 -0
  53. package/src/lib/inference-network.js +407 -0
  54. package/src/lib/ipfs-storage.js +575 -0
  55. package/src/lib/knowledge-graph.js +530 -0
  56. package/src/lib/mcp-client.js +3 -0
  57. package/src/lib/multimodal.js +725 -0
  58. package/src/lib/nl-programming.js +595 -0
  59. package/src/lib/perception.js +500 -0
  60. package/src/lib/pqc-manager.js +141 -9
  61. package/src/lib/privacy-computing.js +575 -0
  62. package/src/lib/protocol-fusion.js +535 -0
  63. package/src/lib/quantization.js +362 -0
  64. package/src/lib/reputation-optimizer.js +509 -0
  65. package/src/lib/skill-marketplace.js +397 -0
  66. package/src/lib/sla-manager.js +484 -0
  67. package/src/lib/stress-tester.js +383 -0
  68. package/src/lib/tech-learning-engine.js +651 -0
  69. package/src/lib/tenant-saas.js +831 -0
  70. package/src/lib/token-incentive.js +513 -0
  71. package/src/lib/trust-security.js +473 -0
  72. package/src/lib/universal-runtime.js +771 -0
  73. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
@@ -0,0 +1,575 @@
1
+ /**
2
+ * Privacy Computing — CLI port of Phase 91 隐私计算框架
3
+ * (docs/design/modules/56_隐私计算框架.md).
4
+ *
5
+ * Desktop uses real FedAvg, Shamir MPC, Laplace DP, and Paillier HE
6
+ * with Pinia store + PrivacyComputingPage.vue. CLI port ships:
7
+ *
8
+ * - Federated learning model lifecycle (init → train → aggregate → complete)
9
+ * - MPC computation tracking (Shamir/Beaver/GMW protocols)
10
+ * - Differential privacy publish with Laplace/Gaussian noise
11
+ * - Homomorphic encryption query simulation
12
+ * - Privacy budget tracking + report
13
+ *
14
+ * What does NOT port: real cryptographic primitives (Shamir polynomials,
15
+ * Paillier key gen), PrivacyComputingPage.vue, Pinia store.
16
+ */
17
+
18
+ import crypto from "crypto";
19
+
20
+ /* ── Constants ─────────────────────────────────────────────── */
21
+
22
+ export const FL_STATUS = Object.freeze({
23
+ INITIALIZING: "initializing",
24
+ TRAINING: "training",
25
+ AGGREGATING: "aggregating",
26
+ COMPLETED: "completed",
27
+ FAILED: "failed",
28
+ });
29
+
30
+ export const MPC_PROTOCOL = Object.freeze({
31
+ SHAMIR: Object.freeze({
32
+ id: "shamir",
33
+ name: "Shamir Secret Sharing",
34
+ description: "Shamir (t,n) 秘密共享",
35
+ }),
36
+ BEAVER: Object.freeze({
37
+ id: "beaver",
38
+ name: "Beaver Triples",
39
+ description: "Beaver 三元组乘法",
40
+ }),
41
+ GMW: Object.freeze({
42
+ id: "gmw",
43
+ name: "GMW Protocol",
44
+ description: "Goldreich-Micali-Wigderson",
45
+ }),
46
+ });
47
+
48
+ export const DP_MECHANISM = Object.freeze({
49
+ LAPLACE: Object.freeze({
50
+ id: "laplace",
51
+ name: "Laplace",
52
+ description: "Laplace 噪声",
53
+ }),
54
+ GAUSSIAN: Object.freeze({
55
+ id: "gaussian",
56
+ name: "Gaussian",
57
+ description: "高斯噪声",
58
+ }),
59
+ EXPONENTIAL: Object.freeze({
60
+ id: "exponential",
61
+ name: "Exponential",
62
+ description: "指数机制",
63
+ }),
64
+ });
65
+
66
+ export const HE_SCHEME = Object.freeze({
67
+ PAILLIER: Object.freeze({
68
+ id: "paillier",
69
+ name: "Paillier",
70
+ description: "加法同态",
71
+ }),
72
+ BFV: Object.freeze({ id: "bfv", name: "BFV", description: "全同态 (整数)" }),
73
+ CKKS: Object.freeze({
74
+ id: "ckks",
75
+ name: "CKKS",
76
+ description: "全同态 (浮点)",
77
+ }),
78
+ });
79
+
80
+ export const DEFAULT_CONFIG = Object.freeze({
81
+ maxRounds: 100,
82
+ minParticipants: 3,
83
+ aggregationStrategy: "fedavg",
84
+ defaultEpsilon: 1.0,
85
+ defaultDelta: 1e-5,
86
+ maxBudget: 10.0,
87
+ mpcTimeoutMs: 30000,
88
+ });
89
+
90
+ /* ── State ─────────────────────────────────────────────── */
91
+
92
+ let _models = new Map();
93
+ let _computations = new Map();
94
+ let _privacyBudget = { spent: 0, limit: DEFAULT_CONFIG.maxBudget };
95
+
96
+ function _id() {
97
+ return crypto.randomUUID();
98
+ }
99
+ function _now() {
100
+ return Date.now();
101
+ }
102
+
103
+ function _strip(row) {
104
+ if (!row) return null;
105
+ const out = {};
106
+ for (const [k, v] of Object.entries(row)) {
107
+ if (k !== "_rowid_" && k !== "rowid") out[k] = v;
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /* ── Schema ────────────────────────────────────────────── */
113
+
114
+ export function ensurePrivacyTables(db) {
115
+ db.exec(`CREATE TABLE IF NOT EXISTS fl_models (
116
+ id TEXT PRIMARY KEY,
117
+ name TEXT NOT NULL,
118
+ model_type TEXT,
119
+ architecture TEXT,
120
+ status TEXT DEFAULT 'initializing',
121
+ current_round INTEGER DEFAULT 0,
122
+ total_rounds INTEGER,
123
+ participant_count INTEGER DEFAULT 0,
124
+ accuracy REAL DEFAULT 0.0,
125
+ loss REAL,
126
+ learning_rate REAL DEFAULT 0.01,
127
+ aggregation_strategy TEXT DEFAULT 'fedavg',
128
+ privacy_budget_spent REAL DEFAULT 0.0,
129
+ created_at INTEGER,
130
+ updated_at INTEGER
131
+ )`);
132
+ db.exec("CREATE INDEX IF NOT EXISTS idx_flm_status ON fl_models(status)");
133
+
134
+ db.exec(`CREATE TABLE IF NOT EXISTS mpc_computations (
135
+ id TEXT PRIMARY KEY,
136
+ computation_type TEXT NOT NULL,
137
+ protocol TEXT DEFAULT 'shamir',
138
+ participant_count INTEGER,
139
+ participant_ids TEXT,
140
+ result_hash TEXT,
141
+ status TEXT DEFAULT 'pending',
142
+ shares_received INTEGER DEFAULT 0,
143
+ shares_required INTEGER,
144
+ computation_time_ms REAL,
145
+ error_message TEXT,
146
+ created_at INTEGER,
147
+ completed_at INTEGER
148
+ )`);
149
+ db.exec(
150
+ "CREATE INDEX IF NOT EXISTS idx_mpc_status ON mpc_computations(status)",
151
+ );
152
+
153
+ _loadAll(db);
154
+ }
155
+
156
+ function _loadAll(db) {
157
+ _models.clear();
158
+ _computations.clear();
159
+ _privacyBudget = { spent: 0, limit: DEFAULT_CONFIG.maxBudget };
160
+
161
+ try {
162
+ for (const row of db.prepare("SELECT * FROM fl_models").all()) {
163
+ const m = _strip(row);
164
+ _models.set(m.id, m);
165
+ _privacyBudget.spent += m.privacy_budget_spent || 0;
166
+ }
167
+ } catch (_e) {
168
+ /* table may not exist */
169
+ }
170
+ try {
171
+ for (const row of db.prepare("SELECT * FROM mpc_computations").all()) {
172
+ const c = _strip(row);
173
+ c.participant_ids = _parseJson(c.participant_ids, []);
174
+ _computations.set(c.id, c);
175
+ }
176
+ } catch (_e) {
177
+ /* table may not exist */
178
+ }
179
+ }
180
+
181
+ function _parseJson(str, fallback) {
182
+ if (!str) return fallback;
183
+ try {
184
+ return JSON.parse(str);
185
+ } catch (_e) {
186
+ return fallback;
187
+ }
188
+ }
189
+
190
+ /* ── Federated Learning ────────────────────────────────── */
191
+
192
+ export function createModel(
193
+ db,
194
+ name,
195
+ { modelType, architecture, totalRounds, learningRate, participants } = {},
196
+ ) {
197
+ const id = _id();
198
+ const now = _now();
199
+ const rounds = totalRounds || 10;
200
+ const lr = learningRate || 0.01;
201
+ const participantCount = participants || 0;
202
+
203
+ const model = {
204
+ id,
205
+ name,
206
+ model_type: modelType || "neural_network",
207
+ architecture: architecture || "mlp",
208
+ status: "initializing",
209
+ current_round: 0,
210
+ total_rounds: rounds,
211
+ participant_count: participantCount,
212
+ accuracy: 0,
213
+ loss: null,
214
+ learning_rate: lr,
215
+ aggregation_strategy: DEFAULT_CONFIG.aggregationStrategy,
216
+ privacy_budget_spent: 0,
217
+ created_at: now,
218
+ updated_at: now,
219
+ };
220
+
221
+ db.prepare(
222
+ `INSERT INTO fl_models (id, name, model_type, architecture, status, current_round, total_rounds,
223
+ participant_count, accuracy, loss, learning_rate, aggregation_strategy, privacy_budget_spent, created_at, updated_at)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
225
+ ).run(
226
+ id,
227
+ name,
228
+ model.model_type,
229
+ model.architecture,
230
+ "initializing",
231
+ 0,
232
+ rounds,
233
+ participantCount,
234
+ 0,
235
+ null,
236
+ lr,
237
+ model.aggregation_strategy,
238
+ 0,
239
+ now,
240
+ now,
241
+ );
242
+
243
+ _models.set(id, model);
244
+ return { modelId: id };
245
+ }
246
+
247
+ export function trainRound(db, modelId) {
248
+ const m = _models.get(modelId);
249
+ if (!m) return { trained: false, reason: "not_found" };
250
+ if (m.status === "completed")
251
+ return { trained: false, reason: "already_completed" };
252
+ if (m.status === "failed") return { trained: false, reason: "model_failed" };
253
+
254
+ if (m.status === "initializing") m.status = "training";
255
+
256
+ m.current_round += 1;
257
+ // Simulated accuracy improvement
258
+ const baseAcc = 0.5;
259
+ const progress = m.current_round / m.total_rounds;
260
+ m.accuracy = Math.round((baseAcc + progress * 0.45) * 1000) / 1000;
261
+ m.loss = Math.round((1 - m.accuracy) * 0.5 * 1000) / 1000;
262
+ m.privacy_budget_spent += DEFAULT_CONFIG.defaultEpsilon * 0.1;
263
+ m.updated_at = _now();
264
+
265
+ if (m.current_round >= m.total_rounds) {
266
+ m.status = "completed";
267
+ }
268
+
269
+ db.prepare(
270
+ `UPDATE fl_models SET status = ?, current_round = ?, accuracy = ?, loss = ?,
271
+ privacy_budget_spent = ?, updated_at = ? WHERE id = ?`,
272
+ ).run(
273
+ m.status,
274
+ m.current_round,
275
+ m.accuracy,
276
+ m.loss,
277
+ m.privacy_budget_spent,
278
+ m.updated_at,
279
+ modelId,
280
+ );
281
+
282
+ return {
283
+ trained: true,
284
+ round: m.current_round,
285
+ accuracy: m.accuracy,
286
+ status: m.status,
287
+ };
288
+ }
289
+
290
+ export function failModel(db, modelId, reason) {
291
+ const m = _models.get(modelId);
292
+ if (!m) return { failed: false, reason: "not_found" };
293
+ m.status = "failed";
294
+ m.updated_at = _now();
295
+ db.prepare(
296
+ "UPDATE fl_models SET status = ?, updated_at = ? WHERE id = ?",
297
+ ).run("failed", m.updated_at, modelId);
298
+ return { failed: true };
299
+ }
300
+
301
+ export function getModel(db, modelId) {
302
+ const m = _models.get(modelId);
303
+ return m ? { ...m } : null;
304
+ }
305
+
306
+ export function listModels(db, { status, limit = 50 } = {}) {
307
+ let models = [..._models.values()];
308
+ if (status) models = models.filter((m) => m.status === status);
309
+ return models
310
+ .sort((a, b) => b.updated_at - a.updated_at)
311
+ .slice(0, limit)
312
+ .map((m) => ({ ...m }));
313
+ }
314
+
315
+ /* ── MPC Computation ───────────────────────────────────── */
316
+
317
+ const VALID_PROTOCOLS = new Set(Object.values(MPC_PROTOCOL).map((p) => p.id));
318
+
319
+ export function createComputation(
320
+ db,
321
+ computationType,
322
+ { protocol, participantIds, sharesRequired } = {},
323
+ ) {
324
+ const proto = protocol || "shamir";
325
+ if (!VALID_PROTOCOLS.has(proto))
326
+ return { computationId: null, reason: "invalid_protocol" };
327
+
328
+ const ids = participantIds || [];
329
+ const required = sharesRequired || Math.ceil(ids.length / 2) || 2;
330
+
331
+ const id = _id();
332
+ const now = _now();
333
+ const comp = {
334
+ id,
335
+ computation_type: computationType,
336
+ protocol: proto,
337
+ participant_count: ids.length,
338
+ participant_ids: ids,
339
+ result_hash: null,
340
+ status: "pending",
341
+ shares_received: 0,
342
+ shares_required: required,
343
+ computation_time_ms: null,
344
+ error_message: null,
345
+ created_at: now,
346
+ completed_at: null,
347
+ };
348
+
349
+ db.prepare(
350
+ `INSERT INTO mpc_computations (id, computation_type, protocol, participant_count, participant_ids,
351
+ result_hash, status, shares_received, shares_required, computation_time_ms, error_message, created_at, completed_at)
352
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
353
+ ).run(
354
+ id,
355
+ computationType,
356
+ proto,
357
+ ids.length,
358
+ JSON.stringify(ids),
359
+ null,
360
+ "pending",
361
+ 0,
362
+ required,
363
+ null,
364
+ null,
365
+ now,
366
+ null,
367
+ );
368
+
369
+ _computations.set(id, comp);
370
+ return { computationId: id };
371
+ }
372
+
373
+ export function submitShare(db, computationId) {
374
+ const c = _computations.get(computationId);
375
+ if (!c) return { submitted: false, reason: "not_found" };
376
+ if (c.status === "completed")
377
+ return { submitted: false, reason: "already_completed" };
378
+
379
+ c.shares_received += 1;
380
+ if (c.status === "pending") c.status = "computing";
381
+
382
+ if (c.shares_received >= c.shares_required) {
383
+ c.status = "completed";
384
+ c.completed_at = _now();
385
+ c.computation_time_ms = c.completed_at - c.created_at;
386
+ c.result_hash = crypto
387
+ .createHash("sha256")
388
+ .update(`result-${c.id}`)
389
+ .digest("hex")
390
+ .slice(0, 16);
391
+ }
392
+
393
+ db.prepare(
394
+ `UPDATE mpc_computations SET shares_received = ?, status = ?, completed_at = ?,
395
+ computation_time_ms = ?, result_hash = ? WHERE id = ?`,
396
+ ).run(
397
+ c.shares_received,
398
+ c.status,
399
+ c.completed_at,
400
+ c.computation_time_ms,
401
+ c.result_hash,
402
+ computationId,
403
+ );
404
+
405
+ return {
406
+ submitted: true,
407
+ sharesReceived: c.shares_received,
408
+ status: c.status,
409
+ };
410
+ }
411
+
412
+ export function getComputation(db, computationId) {
413
+ const c = _computations.get(computationId);
414
+ return c ? { ...c } : null;
415
+ }
416
+
417
+ export function listComputations(db, { protocol, status, limit = 50 } = {}) {
418
+ let comps = [..._computations.values()];
419
+ if (protocol) comps = comps.filter((c) => c.protocol === protocol);
420
+ if (status) comps = comps.filter((c) => c.status === status);
421
+ return comps
422
+ .sort((a, b) => b.created_at - a.created_at)
423
+ .slice(0, limit)
424
+ .map((c) => ({ ...c }));
425
+ }
426
+
427
+ /* ── Differential Privacy ──────────────────────────────── */
428
+
429
+ export function dpPublish(
430
+ db,
431
+ { data, epsilon, delta, mechanism, sensitivity } = {},
432
+ ) {
433
+ const eps = epsilon || DEFAULT_CONFIG.defaultEpsilon;
434
+ const del = delta || DEFAULT_CONFIG.defaultDelta;
435
+ const mech = mechanism || "laplace";
436
+ const sens = sensitivity || 1.0;
437
+
438
+ // Check budget
439
+ if (_privacyBudget.spent + eps > _privacyBudget.limit) {
440
+ return {
441
+ published: false,
442
+ reason: "budget_exceeded",
443
+ remaining: _privacyBudget.limit - _privacyBudget.spent,
444
+ };
445
+ }
446
+
447
+ // Simulate noise addition
448
+ let noisyData;
449
+ if (Array.isArray(data)) {
450
+ noisyData = data.map((v) => {
451
+ const noise = _generateNoise(mech, sens / eps);
452
+ return typeof v === "number" ? Math.round((v + noise) * 1000) / 1000 : v;
453
+ });
454
+ } else if (typeof data === "number") {
455
+ const noise = _generateNoise(mech, sens / eps);
456
+ noisyData = Math.round((data + noise) * 1000) / 1000;
457
+ } else {
458
+ noisyData = data;
459
+ }
460
+
461
+ _privacyBudget.spent += eps;
462
+
463
+ return {
464
+ published: true,
465
+ data: noisyData,
466
+ epsilon: eps,
467
+ delta: del,
468
+ mechanism: mech,
469
+ budgetSpent: Math.round(_privacyBudget.spent * 1000) / 1000,
470
+ budgetRemaining:
471
+ Math.round((_privacyBudget.limit - _privacyBudget.spent) * 1000) / 1000,
472
+ };
473
+ }
474
+
475
+ function _generateNoise(mechanism, scale) {
476
+ // Simplified noise generation for CLI simulation
477
+ if (mechanism === "laplace") {
478
+ // Laplace noise: double exponential
479
+ const u = Math.random() - 0.5;
480
+ return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
481
+ }
482
+ if (mechanism === "gaussian") {
483
+ // Box-Muller transform
484
+ const u1 = Math.random();
485
+ const u2 = Math.random();
486
+ return scale * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
487
+ }
488
+ // Exponential fallback
489
+ return scale * -Math.log(Math.random());
490
+ }
491
+
492
+ /* ── Homomorphic Encryption ────────────────────────────── */
493
+
494
+ const VALID_SCHEMES = new Set(Object.values(HE_SCHEME).map((s) => s.id));
495
+
496
+ export function heQuery({ data, operation, scheme } = {}) {
497
+ const sch = scheme || "paillier";
498
+ if (!VALID_SCHEMES.has(sch))
499
+ return { result: null, reason: "unsupported_scheme" };
500
+
501
+ const validOps = ["sum", "product", "mean", "count"];
502
+ if (!validOps.includes(operation))
503
+ return { result: null, reason: "unsupported_operation" };
504
+
505
+ if (!Array.isArray(data) || data.length === 0)
506
+ return { result: null, reason: "invalid_data" };
507
+
508
+ // Simulate encrypted computation
509
+ let result;
510
+ if (operation === "sum") result = data.reduce((a, b) => a + b, 0);
511
+ else if (operation === "product") result = data.reduce((a, b) => a * b, 1);
512
+ else if (operation === "mean")
513
+ result = data.reduce((a, b) => a + b, 0) / data.length;
514
+ else if (operation === "count") result = data.length;
515
+
516
+ return {
517
+ result: Math.round(result * 1000) / 1000,
518
+ operation,
519
+ scheme: sch,
520
+ inputCount: data.length,
521
+ encrypted: true, // Simulated — in real impl this would be ciphertext
522
+ };
523
+ }
524
+
525
+ /* ── Privacy Report ────────────────────────────────────── */
526
+
527
+ export function getPrivacyReport(db) {
528
+ const models = [..._models.values()];
529
+ const comps = [..._computations.values()];
530
+
531
+ const flStats = {
532
+ totalModels: models.length,
533
+ completed: models.filter((m) => m.status === "completed").length,
534
+ training: models.filter((m) => m.status === "training").length,
535
+ avgAccuracy:
536
+ models.length > 0
537
+ ? Math.round(
538
+ (models.reduce((s, m) => s + m.accuracy, 0) / models.length) * 1000,
539
+ ) / 1000
540
+ : 0,
541
+ };
542
+
543
+ const mpcStats = {
544
+ totalComputations: comps.length,
545
+ completed: comps.filter((c) => c.status === "completed").length,
546
+ pending: comps.filter(
547
+ (c) => c.status === "pending" || c.status === "computing",
548
+ ).length,
549
+ byProtocol: {},
550
+ };
551
+ for (const c of comps) {
552
+ mpcStats.byProtocol[c.protocol] =
553
+ (mpcStats.byProtocol[c.protocol] || 0) + 1;
554
+ }
555
+
556
+ return {
557
+ privacyBudget: {
558
+ spent: Math.round(_privacyBudget.spent * 1000) / 1000,
559
+ limit: _privacyBudget.limit,
560
+ remaining:
561
+ Math.round((_privacyBudget.limit - _privacyBudget.spent) * 1000) / 1000,
562
+ exhausted: _privacyBudget.spent >= _privacyBudget.limit,
563
+ },
564
+ federatedLearning: flStats,
565
+ mpc: mpcStats,
566
+ };
567
+ }
568
+
569
+ /* ── Reset (tests) ─────────────────────────────────────── */
570
+
571
+ export function _resetState() {
572
+ _models.clear();
573
+ _computations.clear();
574
+ _privacyBudget = { spent: 0, limit: DEFAULT_CONFIG.maxBudget };
575
+ }