chainlesschain 0.66.0 → 0.81.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/package.json +1 -1
- package/src/commands/a2a.js +380 -0
- package/src/commands/bi.js +348 -0
- package/src/commands/crosschain.js +218 -0
- package/src/commands/dlp.js +341 -0
- package/src/commands/evomap.js +394 -0
- package/src/commands/federation.js +283 -0
- package/src/commands/inference.js +318 -0
- package/src/commands/lowcode.js +356 -0
- package/src/commands/marketplace.js +256 -0
- package/src/commands/privacy.js +321 -0
- package/src/commands/reputation.js +261 -0
- package/src/commands/siem.js +246 -0
- package/src/commands/sla.js +259 -0
- package/src/commands/stress.js +230 -0
- package/src/commands/terraform.js +245 -0
- package/src/commands/zkp.js +335 -0
- package/src/lib/a2a-protocol.js +451 -0
- package/src/lib/app-builder.js +239 -0
- package/src/lib/bi-engine.js +338 -0
- package/src/lib/cross-chain.js +345 -0
- package/src/lib/dlp-engine.js +389 -0
- package/src/lib/evomap-federation.js +177 -0
- package/src/lib/evomap-governance.js +276 -0
- package/src/lib/federation-hardening.js +259 -0
- package/src/lib/inference-network.js +330 -0
- package/src/lib/privacy-computing.js +427 -0
- package/src/lib/reputation-optimizer.js +299 -0
- package/src/lib/siem-exporter.js +333 -0
- package/src/lib/skill-marketplace.js +325 -0
- package/src/lib/sla-manager.js +275 -0
- package/src/lib/stress-tester.js +330 -0
- package/src/lib/terraform-manager.js +363 -0
- package/src/lib/zkp-engine.js +274 -0
|
@@ -248,3 +248,279 @@ export function _resetState() {
|
|
|
248
248
|
_ownerships.clear();
|
|
249
249
|
_proposals.clear();
|
|
250
250
|
}
|
|
251
|
+
|
|
252
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
253
|
+
* V2 Canonical Surface (Phase 42 — EvoMap Advanced Governance)
|
|
254
|
+
* Strictly additive; legacy exports above remain unchanged.
|
|
255
|
+
* ═══════════════════════════════════════════════════════════════ */
|
|
256
|
+
|
|
257
|
+
export const PROPOSAL_STATUS_V2 = Object.freeze({
|
|
258
|
+
DRAFT: "draft",
|
|
259
|
+
ACTIVE: "active",
|
|
260
|
+
PASSED: "passed",
|
|
261
|
+
REJECTED: "rejected",
|
|
262
|
+
EXECUTED: "executed",
|
|
263
|
+
EXPIRED: "expired",
|
|
264
|
+
CANCELLED: "cancelled",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export const PROPOSAL_TYPE = Object.freeze({
|
|
268
|
+
STANDARD: "standard",
|
|
269
|
+
GENE_STANDARD: "gene_standard",
|
|
270
|
+
CONFIG_CHANGE: "config_change",
|
|
271
|
+
DISPUTE: "dispute",
|
|
272
|
+
FUNDING: "funding",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export const VOTE_DIRECTION = Object.freeze({
|
|
276
|
+
FOR: "for",
|
|
277
|
+
AGAINST: "against",
|
|
278
|
+
ABSTAIN: "abstain",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const _allowedProposalTransitions = new Map([
|
|
282
|
+
["draft", new Set(["active", "cancelled"])],
|
|
283
|
+
["active", new Set(["passed", "rejected", "expired", "cancelled"])],
|
|
284
|
+
["passed", new Set(["executed"])],
|
|
285
|
+
["rejected", new Set([])],
|
|
286
|
+
["executed", new Set([])],
|
|
287
|
+
["expired", new Set([])],
|
|
288
|
+
["cancelled", new Set([])],
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
export function createGovernanceProposalV2(db, options = {}) {
|
|
292
|
+
const {
|
|
293
|
+
title,
|
|
294
|
+
description,
|
|
295
|
+
proposerDid,
|
|
296
|
+
type,
|
|
297
|
+
votingDurationMs,
|
|
298
|
+
quorum,
|
|
299
|
+
threshold,
|
|
300
|
+
} = options;
|
|
301
|
+
|
|
302
|
+
if (!title) throw new Error("Title is required");
|
|
303
|
+
const proposalType = type || PROPOSAL_TYPE.STANDARD;
|
|
304
|
+
if (!Object.values(PROPOSAL_TYPE).includes(proposalType)) {
|
|
305
|
+
throw new Error(`Unknown proposal type: ${proposalType}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const quorumValue = typeof quorum === "number" ? quorum : 3;
|
|
309
|
+
const thresholdValue = typeof threshold === "number" ? threshold : 0.5;
|
|
310
|
+
if (quorumValue < 1) throw new Error("Quorum must be >= 1");
|
|
311
|
+
if (thresholdValue <= 0 || thresholdValue > 1) {
|
|
312
|
+
throw new Error("Threshold must be in (0, 1]");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const id = crypto.randomUUID();
|
|
316
|
+
const now = new Date().toISOString();
|
|
317
|
+
const duration = votingDurationMs || 7 * 24 * 60 * 60 * 1000;
|
|
318
|
+
const deadline = new Date(Date.now() + duration).toISOString();
|
|
319
|
+
|
|
320
|
+
const proposal = {
|
|
321
|
+
id,
|
|
322
|
+
title,
|
|
323
|
+
description: description || "",
|
|
324
|
+
proposerDid: proposerDid || "anonymous",
|
|
325
|
+
type: proposalType,
|
|
326
|
+
status: PROPOSAL_STATUS_V2.ACTIVE,
|
|
327
|
+
votesFor: 0,
|
|
328
|
+
votesAgainst: 0,
|
|
329
|
+
votesAbstain: 0,
|
|
330
|
+
weightFor: 0,
|
|
331
|
+
weightAgainst: 0,
|
|
332
|
+
weightAbstain: 0,
|
|
333
|
+
quorum: quorumValue,
|
|
334
|
+
threshold: thresholdValue,
|
|
335
|
+
votingDeadline: deadline,
|
|
336
|
+
executedAt: null,
|
|
337
|
+
createdAt: now,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
_proposals.set(id, proposal);
|
|
341
|
+
|
|
342
|
+
db.prepare(
|
|
343
|
+
`INSERT INTO evomap_governance_proposals (id, title, description, proposer_did, type, status, votes_for, votes_against, quorum_reached, voting_deadline, executed_at, created_at)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
345
|
+
).run(
|
|
346
|
+
id,
|
|
347
|
+
proposal.title,
|
|
348
|
+
proposal.description,
|
|
349
|
+
proposal.proposerDid,
|
|
350
|
+
proposal.type,
|
|
351
|
+
proposal.status,
|
|
352
|
+
0,
|
|
353
|
+
0,
|
|
354
|
+
0,
|
|
355
|
+
deadline,
|
|
356
|
+
null,
|
|
357
|
+
now,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return proposal;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function castVoteV2(db, options = {}) {
|
|
364
|
+
const { proposalId, voterDid, direction, weight } = options;
|
|
365
|
+
const proposal = _proposals.get(proposalId);
|
|
366
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
367
|
+
if (proposal.status !== PROPOSAL_STATUS_V2.ACTIVE) {
|
|
368
|
+
throw new Error(`Proposal is not active: ${proposal.status}`);
|
|
369
|
+
}
|
|
370
|
+
if (!Object.values(VOTE_DIRECTION).includes(direction)) {
|
|
371
|
+
throw new Error(`Unknown vote direction: ${direction}`);
|
|
372
|
+
}
|
|
373
|
+
const weightValue = typeof weight === "number" ? weight : 1;
|
|
374
|
+
if (weightValue <= 0) throw new Error("Vote weight must be positive");
|
|
375
|
+
|
|
376
|
+
if (direction === VOTE_DIRECTION.FOR) {
|
|
377
|
+
proposal.votesFor++;
|
|
378
|
+
proposal.weightFor += weightValue;
|
|
379
|
+
} else if (direction === VOTE_DIRECTION.AGAINST) {
|
|
380
|
+
proposal.votesAgainst++;
|
|
381
|
+
proposal.weightAgainst += weightValue;
|
|
382
|
+
} else {
|
|
383
|
+
proposal.votesAbstain++;
|
|
384
|
+
proposal.weightAbstain += weightValue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const totalVotes =
|
|
388
|
+
proposal.votesFor + proposal.votesAgainst + proposal.votesAbstain;
|
|
389
|
+
const decisiveVotes = proposal.votesFor + proposal.votesAgainst;
|
|
390
|
+
const decisiveWeight = proposal.weightFor + proposal.weightAgainst;
|
|
391
|
+
|
|
392
|
+
if (totalVotes >= proposal.quorum) {
|
|
393
|
+
const ratio =
|
|
394
|
+
decisiveWeight === 0 ? 0 : proposal.weightFor / decisiveWeight;
|
|
395
|
+
if (ratio >= proposal.threshold) {
|
|
396
|
+
proposal.status = PROPOSAL_STATUS_V2.PASSED;
|
|
397
|
+
} else {
|
|
398
|
+
proposal.status = PROPOSAL_STATUS_V2.REJECTED;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
db.prepare(
|
|
402
|
+
`UPDATE evomap_governance_proposals SET votes_for = ?, votes_against = ?, quorum_reached = ?, status = ? WHERE id = ?`,
|
|
403
|
+
).run(
|
|
404
|
+
proposal.votesFor,
|
|
405
|
+
proposal.votesAgainst,
|
|
406
|
+
1,
|
|
407
|
+
proposal.status,
|
|
408
|
+
proposalId,
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
db.prepare(
|
|
412
|
+
`UPDATE evomap_governance_proposals SET votes_for = ?, votes_against = ? WHERE id = ?`,
|
|
413
|
+
).run(proposal.votesFor, proposal.votesAgainst, proposalId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
proposalId,
|
|
418
|
+
direction,
|
|
419
|
+
weight: weightValue,
|
|
420
|
+
totalVotes,
|
|
421
|
+
decisiveVotes,
|
|
422
|
+
status: proposal.status,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function setProposalStatus(db, proposalId, newStatus) {
|
|
427
|
+
const proposal = _proposals.get(proposalId);
|
|
428
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
429
|
+
const validStatuses = Object.values(PROPOSAL_STATUS_V2);
|
|
430
|
+
if (!validStatuses.includes(newStatus)) {
|
|
431
|
+
throw new Error(`Unknown proposal status: ${newStatus}`);
|
|
432
|
+
}
|
|
433
|
+
const allowed = _allowedProposalTransitions.get(proposal.status);
|
|
434
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`Invalid proposal status transition: ${proposal.status} → ${newStatus}`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
proposal.status = newStatus;
|
|
440
|
+
if (newStatus === PROPOSAL_STATUS_V2.EXECUTED) {
|
|
441
|
+
proposal.executedAt = new Date().toISOString();
|
|
442
|
+
}
|
|
443
|
+
db.prepare(
|
|
444
|
+
`UPDATE evomap_governance_proposals SET status = ?, executed_at = ? WHERE id = ?`,
|
|
445
|
+
).run(newStatus, proposal.executedAt, proposalId);
|
|
446
|
+
|
|
447
|
+
return { proposalId, status: newStatus };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function executeProposal(db, proposalId) {
|
|
451
|
+
return setProposalStatus(db, proposalId, PROPOSAL_STATUS_V2.EXECUTED);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function cancelProposal(db, proposalId) {
|
|
455
|
+
return setProposalStatus(db, proposalId, PROPOSAL_STATUS_V2.CANCELLED);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function expireProposalsV2(db, now) {
|
|
459
|
+
const cutoff = typeof now === "number" ? now : Date.now();
|
|
460
|
+
const expired = [];
|
|
461
|
+
for (const p of _proposals.values()) {
|
|
462
|
+
if (p.status === PROPOSAL_STATUS_V2.ACTIVE) {
|
|
463
|
+
const deadlineMs = new Date(p.votingDeadline).getTime();
|
|
464
|
+
if (deadlineMs <= cutoff) {
|
|
465
|
+
p.status = PROPOSAL_STATUS_V2.EXPIRED;
|
|
466
|
+
expired.push(p.id);
|
|
467
|
+
db.prepare(
|
|
468
|
+
`UPDATE evomap_governance_proposals SET status = ? WHERE id = ?`,
|
|
469
|
+
).run(PROPOSAL_STATUS_V2.EXPIRED, p.id);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return { expiredCount: expired.length, expiredIds: expired };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function listProposalsV2(db, filter = {}) {
|
|
477
|
+
let proposals = [..._proposals.values()];
|
|
478
|
+
if (filter.status) {
|
|
479
|
+
proposals = proposals.filter((p) => p.status === filter.status);
|
|
480
|
+
}
|
|
481
|
+
if (filter.type) {
|
|
482
|
+
proposals = proposals.filter((p) => p.type === filter.type);
|
|
483
|
+
}
|
|
484
|
+
if (filter.proposerDid) {
|
|
485
|
+
proposals = proposals.filter((p) => p.proposerDid === filter.proposerDid);
|
|
486
|
+
}
|
|
487
|
+
const limit = filter.limit || 100;
|
|
488
|
+
return proposals.slice(0, limit);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function traceContributions(geneId) {
|
|
492
|
+
return traceOwnership(geneId);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function getGovernanceStatsV2() {
|
|
496
|
+
const all = [..._proposals.values()];
|
|
497
|
+
|
|
498
|
+
const byStatus = {};
|
|
499
|
+
for (const status of Object.values(PROPOSAL_STATUS_V2)) {
|
|
500
|
+
byStatus[status] = all.filter((p) => p.status === status).length;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const byType = {};
|
|
504
|
+
for (const type of Object.values(PROPOSAL_TYPE)) {
|
|
505
|
+
byType[type] = all.filter((p) => p.type === type).length;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const totalVotes = all.reduce(
|
|
509
|
+
(s, p) => s + p.votesFor + p.votesAgainst + (p.votesAbstain || 0),
|
|
510
|
+
0,
|
|
511
|
+
);
|
|
512
|
+
const totalWeight = all.reduce(
|
|
513
|
+
(s, p) =>
|
|
514
|
+
s + (p.weightFor || 0) + (p.weightAgainst || 0) + (p.weightAbstain || 0),
|
|
515
|
+
0,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
totalProposals: all.length,
|
|
520
|
+
totalOwnerships: _ownerships.size,
|
|
521
|
+
totalVotes,
|
|
522
|
+
totalWeight,
|
|
523
|
+
byStatus,
|
|
524
|
+
byType,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
@@ -584,4 +584,263 @@ export function _resetState() {
|
|
|
584
584
|
_breakers.clear();
|
|
585
585
|
_healthChecks.clear();
|
|
586
586
|
_pools.clear();
|
|
587
|
+
_nodeStatuses.clear();
|
|
588
|
+
_failureThreshold = FED_DEFAULT_FAILURE_THRESHOLD;
|
|
589
|
+
_halfOpenCooldownMs = FED_DEFAULT_HALF_OPEN_COOLDOWN_MS;
|
|
590
|
+
_unhealthyThreshold = FED_DEFAULT_UNHEALTHY_THRESHOLD;
|
|
591
|
+
_maxActiveNodes = FED_DEFAULT_MAX_ACTIVE_NODES;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* ──────────────────────────────────────────────────────────
|
|
595
|
+
* V2 — Phase 58 surface (strictly additive)
|
|
596
|
+
* ────────────────────────────────────────────────────────── */
|
|
597
|
+
|
|
598
|
+
export const CIRCUIT_STATE_V2 = CIRCUIT_STATE;
|
|
599
|
+
export const HEALTH_STATUS_V2 = HEALTH_STATUS;
|
|
600
|
+
export const HEALTH_METRIC_V2 = HEALTH_METRIC;
|
|
601
|
+
|
|
602
|
+
export const NODE_STATUS_V2 = Object.freeze({
|
|
603
|
+
REGISTERED: "registered",
|
|
604
|
+
ACTIVE: "active",
|
|
605
|
+
ISOLATED: "isolated",
|
|
606
|
+
DECOMMISSIONED: "decommissioned",
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
export const FED_DEFAULT_FAILURE_THRESHOLD = 5;
|
|
610
|
+
export const FED_DEFAULT_HALF_OPEN_COOLDOWN_MS = 60_000;
|
|
611
|
+
export const FED_DEFAULT_UNHEALTHY_THRESHOLD = 3;
|
|
612
|
+
export const FED_DEFAULT_MAX_ACTIVE_NODES = 50;
|
|
613
|
+
|
|
614
|
+
let _nodeStatuses = new Map();
|
|
615
|
+
let _failureThreshold = FED_DEFAULT_FAILURE_THRESHOLD;
|
|
616
|
+
let _halfOpenCooldownMs = FED_DEFAULT_HALF_OPEN_COOLDOWN_MS;
|
|
617
|
+
let _unhealthyThreshold = FED_DEFAULT_UNHEALTHY_THRESHOLD;
|
|
618
|
+
let _maxActiveNodes = FED_DEFAULT_MAX_ACTIVE_NODES;
|
|
619
|
+
|
|
620
|
+
const NODE_TRANSITIONS_V2 = new Map([
|
|
621
|
+
["registered", new Set(["active", "decommissioned"])],
|
|
622
|
+
["active", new Set(["isolated", "decommissioned"])],
|
|
623
|
+
["isolated", new Set(["active", "decommissioned"])],
|
|
624
|
+
]);
|
|
625
|
+
const NODE_TERMINALS_V2 = new Set(["decommissioned"]);
|
|
626
|
+
|
|
627
|
+
/* ── Config ────────────────────────────────────────────── */
|
|
628
|
+
|
|
629
|
+
function _positiveInt(n, label) {
|
|
630
|
+
if (typeof n !== "number" || Number.isNaN(n) || n < 1) {
|
|
631
|
+
throw new Error(`${label} must be a positive integer`);
|
|
632
|
+
}
|
|
633
|
+
return Math.floor(n);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function setFailureThreshold(n) {
|
|
637
|
+
_failureThreshold = _positiveInt(n, "failureThreshold");
|
|
638
|
+
}
|
|
639
|
+
export function getFailureThreshold() {
|
|
640
|
+
return _failureThreshold;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function setHalfOpenCooldownMs(ms) {
|
|
644
|
+
_halfOpenCooldownMs = _positiveInt(ms, "halfOpenCooldownMs");
|
|
645
|
+
}
|
|
646
|
+
export function getHalfOpenCooldownMs() {
|
|
647
|
+
return _halfOpenCooldownMs;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function setUnhealthyThreshold(n) {
|
|
651
|
+
_unhealthyThreshold = _positiveInt(n, "unhealthyThreshold");
|
|
652
|
+
}
|
|
653
|
+
export function getUnhealthyThreshold() {
|
|
654
|
+
return _unhealthyThreshold;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function setMaxActiveNodes(n) {
|
|
658
|
+
_maxActiveNodes = _positiveInt(n, "maxActiveNodes");
|
|
659
|
+
}
|
|
660
|
+
export function getMaxActiveNodes() {
|
|
661
|
+
return _maxActiveNodes;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function getActiveNodeCount() {
|
|
665
|
+
let count = 0;
|
|
666
|
+
for (const s of _nodeStatuses.values()) {
|
|
667
|
+
if (s.status === NODE_STATUS_V2.ACTIVE) count += 1;
|
|
668
|
+
}
|
|
669
|
+
return count;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* ── Node Lifecycle V2 ─────────────────────────────────── */
|
|
673
|
+
|
|
674
|
+
export function registerNodeV2(db, { nodeId, metadata } = {}) {
|
|
675
|
+
if (!nodeId) throw new Error("nodeId is required");
|
|
676
|
+
if (_nodeStatuses.has(nodeId)) {
|
|
677
|
+
throw new Error(`Node already registered: ${nodeId}`);
|
|
678
|
+
}
|
|
679
|
+
const now = _now();
|
|
680
|
+
const entry = {
|
|
681
|
+
node_id: nodeId,
|
|
682
|
+
status: NODE_STATUS_V2.REGISTERED,
|
|
683
|
+
metadata: metadata || null,
|
|
684
|
+
registered_at: now,
|
|
685
|
+
updated_at: now,
|
|
686
|
+
};
|
|
687
|
+
_nodeStatuses.set(nodeId, entry);
|
|
688
|
+
|
|
689
|
+
if (!_breakers.has(nodeId)) {
|
|
690
|
+
registerNode(db, nodeId, {
|
|
691
|
+
failureThreshold: _failureThreshold,
|
|
692
|
+
openTimeout: _halfOpenCooldownMs,
|
|
693
|
+
metadata: metadata || null,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return { ...entry };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function getNodeStatusV2(nodeId) {
|
|
700
|
+
const s = _nodeStatuses.get(nodeId);
|
|
701
|
+
return s ? { ...s } : null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function setNodeStatusV2(db, nodeId, newStatus, patch = {}) {
|
|
705
|
+
const entry = _nodeStatuses.get(nodeId);
|
|
706
|
+
if (!entry) throw new Error(`Node not found: ${nodeId}`);
|
|
707
|
+
if (!Object.values(NODE_STATUS_V2).includes(newStatus)) {
|
|
708
|
+
throw new Error(`Unknown node status: ${newStatus}`);
|
|
709
|
+
}
|
|
710
|
+
if (NODE_TERMINALS_V2.has(entry.status)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`Invalid transition: ${entry.status} → ${newStatus} (terminal)`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
const allowed = NODE_TRANSITIONS_V2.get(entry.status);
|
|
716
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
717
|
+
throw new Error(`Invalid transition: ${entry.status} → ${newStatus}`);
|
|
718
|
+
}
|
|
719
|
+
if (newStatus === NODE_STATUS_V2.ACTIVE) {
|
|
720
|
+
const active = getActiveNodeCount();
|
|
721
|
+
if (entry.status !== NODE_STATUS_V2.ACTIVE && active >= _maxActiveNodes) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`Max active nodes reached (${active}/${_maxActiveNodes})`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
entry.status = newStatus;
|
|
728
|
+
entry.updated_at = _now();
|
|
729
|
+
if (patch.metadata !== undefined) entry.metadata = patch.metadata;
|
|
730
|
+
if (patch.reason !== undefined) entry.reason = patch.reason;
|
|
731
|
+
return { ...entry };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* ── Health Check V2 ───────────────────────────────────── */
|
|
735
|
+
|
|
736
|
+
const VALID_METRIC_V2 = new Set(Object.values(HEALTH_METRIC_V2));
|
|
737
|
+
const VALID_HEALTH_STATUS_V2 = new Set(Object.values(HEALTH_STATUS_V2));
|
|
738
|
+
|
|
739
|
+
export function recordHealthCheckV2(
|
|
740
|
+
db,
|
|
741
|
+
{ nodeId, checkType, status, metrics } = {},
|
|
742
|
+
) {
|
|
743
|
+
if (!nodeId) throw new Error("nodeId is required");
|
|
744
|
+
if (!checkType || !VALID_METRIC_V2.has(checkType)) {
|
|
745
|
+
throw new Error(`Invalid check type: ${checkType}`);
|
|
746
|
+
}
|
|
747
|
+
if (!status || !VALID_HEALTH_STATUS_V2.has(status)) {
|
|
748
|
+
throw new Error(`Invalid status: ${status}`);
|
|
749
|
+
}
|
|
750
|
+
if (metrics && typeof metrics === "object") {
|
|
751
|
+
for (const [k, v] of Object.entries(metrics)) {
|
|
752
|
+
if (typeof v === "number" && Number.isNaN(v)) {
|
|
753
|
+
throw new Error(`Metric ${k} is NaN`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const result = recordHealthCheck(db, { nodeId, checkType, status, metrics });
|
|
758
|
+
if (!result.recorded) throw new Error(`Failed to record: ${result.reason}`);
|
|
759
|
+
return { ...result };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* ── Circuit V2 Shortcuts ──────────────────────────────── */
|
|
763
|
+
|
|
764
|
+
export function tripCircuit(db, nodeId) {
|
|
765
|
+
const b = _breakers.get(nodeId);
|
|
766
|
+
if (!b) throw new Error(`Node not found: ${nodeId}`);
|
|
767
|
+
if (b.state === "open") {
|
|
768
|
+
throw new Error(`Circuit already open: ${nodeId}`);
|
|
769
|
+
}
|
|
770
|
+
const now = _now();
|
|
771
|
+
b.state = "open";
|
|
772
|
+
b.state_changed_at = now;
|
|
773
|
+
b.success_count = 0;
|
|
774
|
+
b.updated_at = now;
|
|
775
|
+
db.prepare(
|
|
776
|
+
`UPDATE federation_circuit_breakers
|
|
777
|
+
SET state = 'open', success_count = 0, state_changed_at = ?, updated_at = ?
|
|
778
|
+
WHERE node_id = ?`,
|
|
779
|
+
).run(now, now, nodeId);
|
|
780
|
+
return { state: "open", nodeId };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* ── Auto-Isolate ──────────────────────────────────────── */
|
|
784
|
+
|
|
785
|
+
export function autoIsolateUnhealthyNodes(db) {
|
|
786
|
+
const isolated = [];
|
|
787
|
+
for (const [nodeId, nodeEntry] of _nodeStatuses.entries()) {
|
|
788
|
+
if (nodeEntry.status !== NODE_STATUS_V2.ACTIVE) continue;
|
|
789
|
+
const checks = [..._healthChecks.values()]
|
|
790
|
+
.filter((c) => c.node_id === nodeId)
|
|
791
|
+
.sort((a, b) => b.checked_at - a.checked_at)
|
|
792
|
+
.slice(0, _unhealthyThreshold);
|
|
793
|
+
if (checks.length < _unhealthyThreshold) continue;
|
|
794
|
+
const allUnhealthy = checks.every(
|
|
795
|
+
(c) => c.status === HEALTH_STATUS_V2.UNHEALTHY,
|
|
796
|
+
);
|
|
797
|
+
if (allUnhealthy) {
|
|
798
|
+
nodeEntry.status = NODE_STATUS_V2.ISOLATED;
|
|
799
|
+
nodeEntry.updated_at = _now();
|
|
800
|
+
nodeEntry.reason = `auto-isolated: ${_unhealthyThreshold} consecutive unhealthy checks`;
|
|
801
|
+
isolated.push({ ...nodeEntry });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return isolated;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* ── V2 Stats ──────────────────────────────────────────── */
|
|
808
|
+
|
|
809
|
+
export function getFederationHardeningStatsV2(db) {
|
|
810
|
+
const circuitsByState = {};
|
|
811
|
+
for (const s of Object.values(CIRCUIT_STATE_V2)) circuitsByState[s] = 0;
|
|
812
|
+
for (const b of _breakers.values()) {
|
|
813
|
+
circuitsByState[b.state] = (circuitsByState[b.state] || 0) + 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const nodesByStatus = {};
|
|
817
|
+
for (const s of Object.values(NODE_STATUS_V2)) nodesByStatus[s] = 0;
|
|
818
|
+
for (const n of _nodeStatuses.values()) {
|
|
819
|
+
nodesByStatus[n.status] = (nodesByStatus[n.status] || 0) + 1;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const healthByStatus = {};
|
|
823
|
+
for (const s of Object.values(HEALTH_STATUS_V2)) healthByStatus[s] = 0;
|
|
824
|
+
const healthByMetric = {};
|
|
825
|
+
for (const m of Object.values(HEALTH_METRIC_V2)) healthByMetric[m] = 0;
|
|
826
|
+
for (const c of _healthChecks.values()) {
|
|
827
|
+
healthByStatus[c.status] = (healthByStatus[c.status] || 0) + 1;
|
|
828
|
+
healthByMetric[c.check_type] = (healthByMetric[c.check_type] || 0) + 1;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
totalNodes: _nodeStatuses.size,
|
|
833
|
+
activeNodes: nodesByStatus[NODE_STATUS_V2.ACTIVE],
|
|
834
|
+
isolatedNodes: nodesByStatus[NODE_STATUS_V2.ISOLATED],
|
|
835
|
+
totalCircuits: _breakers.size,
|
|
836
|
+
totalHealthChecks: _healthChecks.size,
|
|
837
|
+
maxActiveNodes: _maxActiveNodes,
|
|
838
|
+
failureThreshold: _failureThreshold,
|
|
839
|
+
halfOpenCooldownMs: _halfOpenCooldownMs,
|
|
840
|
+
unhealthyThreshold: _unhealthyThreshold,
|
|
841
|
+
circuitsByState,
|
|
842
|
+
nodesByStatus,
|
|
843
|
+
healthByStatus,
|
|
844
|
+
healthByMetric,
|
|
845
|
+
};
|
|
587
846
|
}
|