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.
@@ -17,6 +17,14 @@ import {
17
17
  getPressureReport,
18
18
  recombineGenes,
19
19
  getLineage,
20
+ HUB_STATUS_V2,
21
+ TRUST_TIER,
22
+ MUTATION_TYPE,
23
+ trustTier,
24
+ setHubStatus,
25
+ listHubsV2,
26
+ buildFederationContext,
27
+ getFederationStatsV2,
20
28
  } from "../lib/evomap-federation.js";
21
29
  import {
22
30
  ensureEvoMapGovernanceTables,
@@ -25,6 +33,18 @@ import {
25
33
  createGovernanceProposal,
26
34
  voteOnGovernanceProposal,
27
35
  getGovernanceDashboard,
36
+ PROPOSAL_STATUS_V2,
37
+ PROPOSAL_TYPE,
38
+ VOTE_DIRECTION,
39
+ createGovernanceProposalV2,
40
+ castVoteV2,
41
+ setProposalStatus,
42
+ executeProposal,
43
+ cancelProposal,
44
+ expireProposalsV2,
45
+ listProposalsV2,
46
+ traceContributions,
47
+ getGovernanceStatsV2,
28
48
  } from "../lib/evomap-governance.js";
29
49
 
30
50
  export function registerEvoMapCommand(program) {
@@ -555,4 +575,378 @@ export function registerEvoMapCommand(program) {
555
575
  process.exit(1);
556
576
  }
557
577
  });
578
+
579
+ // ═══════════════════════════════════════════════════════════════
580
+ // V2 Canonical Subcommands (Phase 42)
581
+ // ═══════════════════════════════════════════════════════════════
582
+
583
+ federation
584
+ .command("hub-statuses")
585
+ .description("List V2 hub status values")
586
+ .action(() => {
587
+ console.log(JSON.stringify(Object.values(HUB_STATUS_V2), null, 2));
588
+ });
589
+
590
+ federation
591
+ .command("trust-tiers")
592
+ .description("List V2 trust tier values")
593
+ .action(() => {
594
+ console.log(JSON.stringify(Object.values(TRUST_TIER), null, 2));
595
+ });
596
+
597
+ federation
598
+ .command("mutation-types")
599
+ .description("List V2 mutation types")
600
+ .action(() => {
601
+ console.log(JSON.stringify(Object.values(MUTATION_TYPE), null, 2));
602
+ });
603
+
604
+ federation
605
+ .command("trust-tier <score>")
606
+ .description("Classify a trust score as low|medium|high")
607
+ .action((score) => {
608
+ try {
609
+ const num = Number(score);
610
+ console.log(JSON.stringify({ score: num, tier: trustTier(num) }));
611
+ } catch (err) {
612
+ logger.error(`Failed: ${err.message}`);
613
+ process.exit(1);
614
+ }
615
+ });
616
+
617
+ federation
618
+ .command("set-hub-status <hub-id> <status>")
619
+ .description("Transition a hub's status (V2 state machine)")
620
+ .action(async (hubId, status) => {
621
+ try {
622
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
623
+ if (!ctx.db) {
624
+ logger.error("Database not available");
625
+ process.exit(1);
626
+ }
627
+ const db = ctx.db.getDatabase();
628
+ ensureEvoMapFederationTables(db);
629
+ console.log(JSON.stringify(setHubStatus(db, hubId, status), null, 2));
630
+ await shutdown();
631
+ } catch (err) {
632
+ logger.error(`Failed: ${err.message}`);
633
+ process.exit(1);
634
+ }
635
+ });
636
+
637
+ federation
638
+ .command("list-hubs-v2")
639
+ .description("List hubs with V2 filters (trust tier, minTrust)")
640
+ .option("--status <status>", "Filter by hub status")
641
+ .option("--region <region>", "Filter by region")
642
+ .option("--min-trust <n>", "Minimum trust score (0-1)")
643
+ .option("--trust-tier <tier>", "Filter by trust tier (low|medium|high)")
644
+ .action(async (options) => {
645
+ try {
646
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
647
+ if (!ctx.db) {
648
+ logger.error("Database not available");
649
+ process.exit(1);
650
+ }
651
+ const db = ctx.db.getDatabase();
652
+ ensureEvoMapFederationTables(db);
653
+ const hubs = listHubsV2(db, {
654
+ status: options.status,
655
+ region: options.region,
656
+ minTrust: options.minTrust ? Number(options.minTrust) : undefined,
657
+ trustTier: options.trustTier,
658
+ });
659
+ console.log(JSON.stringify(hubs, null, 2));
660
+ await shutdown();
661
+ } catch (err) {
662
+ logger.error(`Failed: ${err.message}`);
663
+ process.exit(1);
664
+ }
665
+ });
666
+
667
+ federation
668
+ .command("context")
669
+ .description("Build federation context for LLM consumption")
670
+ .action(async () => {
671
+ try {
672
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
673
+ if (!ctx.db) {
674
+ logger.error("Database not available");
675
+ process.exit(1);
676
+ }
677
+ const db = ctx.db.getDatabase();
678
+ ensureEvoMapFederationTables(db);
679
+ console.log(JSON.stringify(buildFederationContext(), null, 2));
680
+ await shutdown();
681
+ } catch (err) {
682
+ logger.error(`Failed: ${err.message}`);
683
+ process.exit(1);
684
+ }
685
+ });
686
+
687
+ federation
688
+ .command("stats-v2")
689
+ .description("Show V2 federation stats (byStatus/byRegion/byTrustTier)")
690
+ .action(async () => {
691
+ try {
692
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
693
+ if (!ctx.db) {
694
+ logger.error("Database not available");
695
+ process.exit(1);
696
+ }
697
+ const db = ctx.db.getDatabase();
698
+ ensureEvoMapFederationTables(db);
699
+ console.log(JSON.stringify(getFederationStatsV2(), null, 2));
700
+ await shutdown();
701
+ } catch (err) {
702
+ logger.error(`Failed: ${err.message}`);
703
+ process.exit(1);
704
+ }
705
+ });
706
+
707
+ gov
708
+ .command("proposal-statuses")
709
+ .description("List V2 proposal status values")
710
+ .action(() => {
711
+ console.log(JSON.stringify(Object.values(PROPOSAL_STATUS_V2), null, 2));
712
+ });
713
+
714
+ gov
715
+ .command("proposal-types")
716
+ .description("List V2 proposal type values")
717
+ .action(() => {
718
+ console.log(JSON.stringify(Object.values(PROPOSAL_TYPE), null, 2));
719
+ });
720
+
721
+ gov
722
+ .command("vote-directions")
723
+ .description("List V2 vote direction values")
724
+ .action(() => {
725
+ console.log(JSON.stringify(Object.values(VOTE_DIRECTION), null, 2));
726
+ });
727
+
728
+ gov
729
+ .command("propose-v2")
730
+ .description("Create a V2 governance proposal with type/quorum/threshold")
731
+ .requiredOption("-t, --title <title>", "Proposal title")
732
+ .option("-d, --description <text>", "Proposal description")
733
+ .option("-p, --proposer <did>", "Proposer DID", "cli-user")
734
+ .option(
735
+ "--type <type>",
736
+ "Proposal type (standard|gene_standard|...)",
737
+ "standard",
738
+ )
739
+ .option("--quorum <n>", "Quorum (min votes)", "3")
740
+ .option("--threshold <n>", "Threshold (0-1)", "0.5")
741
+ .option("--voting-duration-ms <n>", "Voting duration in ms")
742
+ .action(async (options) => {
743
+ try {
744
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
745
+ if (!ctx.db) {
746
+ logger.error("Database not available");
747
+ process.exit(1);
748
+ }
749
+ const db = ctx.db.getDatabase();
750
+ ensureEvoMapGovernanceTables(db);
751
+ const p = createGovernanceProposalV2(db, {
752
+ title: options.title,
753
+ description: options.description,
754
+ proposerDid: options.proposer,
755
+ type: options.type,
756
+ quorum: Number(options.quorum),
757
+ threshold: Number(options.threshold),
758
+ votingDurationMs: options.votingDurationMs
759
+ ? Number(options.votingDurationMs)
760
+ : undefined,
761
+ });
762
+ console.log(JSON.stringify(p, null, 2));
763
+ await shutdown();
764
+ } catch (err) {
765
+ logger.error(`Failed: ${err.message}`);
766
+ process.exit(1);
767
+ }
768
+ });
769
+
770
+ gov
771
+ .command("vote-v2 <proposal-id> <direction>")
772
+ .description("Cast a weighted V2 vote (for|against|abstain)")
773
+ .option("-v, --voter <did>", "Voter DID", "cli-user")
774
+ .option("-w, --weight <n>", "Vote weight", "1")
775
+ .action(async (proposalId, direction, options) => {
776
+ try {
777
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
778
+ if (!ctx.db) {
779
+ logger.error("Database not available");
780
+ process.exit(1);
781
+ }
782
+ const db = ctx.db.getDatabase();
783
+ ensureEvoMapGovernanceTables(db);
784
+ const r = castVoteV2(db, {
785
+ proposalId,
786
+ voterDid: options.voter,
787
+ direction,
788
+ weight: Number(options.weight),
789
+ });
790
+ console.log(JSON.stringify(r, null, 2));
791
+ await shutdown();
792
+ } catch (err) {
793
+ logger.error(`Failed: ${err.message}`);
794
+ process.exit(1);
795
+ }
796
+ });
797
+
798
+ gov
799
+ .command("set-status <proposal-id> <status>")
800
+ .description("Transition a proposal's status (V2 state machine)")
801
+ .action(async (proposalId, status) => {
802
+ try {
803
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
804
+ if (!ctx.db) {
805
+ logger.error("Database not available");
806
+ process.exit(1);
807
+ }
808
+ const db = ctx.db.getDatabase();
809
+ ensureEvoMapGovernanceTables(db);
810
+ console.log(
811
+ JSON.stringify(setProposalStatus(db, proposalId, status), null, 2),
812
+ );
813
+ await shutdown();
814
+ } catch (err) {
815
+ logger.error(`Failed: ${err.message}`);
816
+ process.exit(1);
817
+ }
818
+ });
819
+
820
+ gov
821
+ .command("execute <proposal-id>")
822
+ .description("Execute a passed proposal")
823
+ .action(async (proposalId) => {
824
+ try {
825
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
826
+ if (!ctx.db) {
827
+ logger.error("Database not available");
828
+ process.exit(1);
829
+ }
830
+ const db = ctx.db.getDatabase();
831
+ ensureEvoMapGovernanceTables(db);
832
+ console.log(JSON.stringify(executeProposal(db, proposalId), null, 2));
833
+ await shutdown();
834
+ } catch (err) {
835
+ logger.error(`Failed: ${err.message}`);
836
+ process.exit(1);
837
+ }
838
+ });
839
+
840
+ gov
841
+ .command("cancel <proposal-id>")
842
+ .description("Cancel a proposal")
843
+ .action(async (proposalId) => {
844
+ try {
845
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
846
+ if (!ctx.db) {
847
+ logger.error("Database not available");
848
+ process.exit(1);
849
+ }
850
+ const db = ctx.db.getDatabase();
851
+ ensureEvoMapGovernanceTables(db);
852
+ console.log(JSON.stringify(cancelProposal(db, proposalId), null, 2));
853
+ await shutdown();
854
+ } catch (err) {
855
+ logger.error(`Failed: ${err.message}`);
856
+ process.exit(1);
857
+ }
858
+ });
859
+
860
+ gov
861
+ .command("expire")
862
+ .description("Bulk-expire active proposals past voting deadline (V2)")
863
+ .action(async () => {
864
+ try {
865
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
866
+ if (!ctx.db) {
867
+ logger.error("Database not available");
868
+ process.exit(1);
869
+ }
870
+ const db = ctx.db.getDatabase();
871
+ ensureEvoMapGovernanceTables(db);
872
+ console.log(JSON.stringify(expireProposalsV2(db), null, 2));
873
+ await shutdown();
874
+ } catch (err) {
875
+ logger.error(`Failed: ${err.message}`);
876
+ process.exit(1);
877
+ }
878
+ });
879
+
880
+ gov
881
+ .command("list-v2")
882
+ .description("List proposals with V2 filters")
883
+ .option("--status <status>", "Filter by status")
884
+ .option("--type <type>", "Filter by type")
885
+ .option("--proposer <did>", "Filter by proposer DID")
886
+ .action(async (options) => {
887
+ try {
888
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
889
+ if (!ctx.db) {
890
+ logger.error("Database not available");
891
+ process.exit(1);
892
+ }
893
+ const db = ctx.db.getDatabase();
894
+ ensureEvoMapGovernanceTables(db);
895
+ console.log(
896
+ JSON.stringify(
897
+ listProposalsV2(db, {
898
+ status: options.status,
899
+ type: options.type,
900
+ proposerDid: options.proposer,
901
+ }),
902
+ null,
903
+ 2,
904
+ ),
905
+ );
906
+ await shutdown();
907
+ } catch (err) {
908
+ logger.error(`Failed: ${err.message}`);
909
+ process.exit(1);
910
+ }
911
+ });
912
+
913
+ gov
914
+ .command("contributions <gene-id>")
915
+ .description("Trace gene contributions (V2 alias of ownership-trace)")
916
+ .action(async (geneId) => {
917
+ try {
918
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
919
+ if (!ctx.db) {
920
+ logger.error("Database not available");
921
+ process.exit(1);
922
+ }
923
+ const db = ctx.db.getDatabase();
924
+ ensureEvoMapGovernanceTables(db);
925
+ console.log(JSON.stringify(traceContributions(geneId), null, 2));
926
+ await shutdown();
927
+ } catch (err) {
928
+ logger.error(`Failed: ${err.message}`);
929
+ process.exit(1);
930
+ }
931
+ });
932
+
933
+ gov
934
+ .command("stats-v2")
935
+ .description("Show V2 governance stats (byStatus/byType, weight totals)")
936
+ .action(async () => {
937
+ try {
938
+ const ctx = await bootstrap({ verbose: program.opts().verbose });
939
+ if (!ctx.db) {
940
+ logger.error("Database not available");
941
+ process.exit(1);
942
+ }
943
+ const db = ctx.db.getDatabase();
944
+ ensureEvoMapGovernanceTables(db);
945
+ console.log(JSON.stringify(getGovernanceStatsV2(), null, 2));
946
+ await shutdown();
947
+ } catch (err) {
948
+ logger.error(`Failed: ${err.message}`);
949
+ process.exit(1);
950
+ }
951
+ });
558
952
  }
@@ -29,6 +29,28 @@ import {
29
29
  listPools,
30
30
  destroyPool,
31
31
  getFederationHardeningStats,
32
+ // V2 (Phase 58)
33
+ NODE_STATUS_V2,
34
+ FED_DEFAULT_FAILURE_THRESHOLD,
35
+ FED_DEFAULT_HALF_OPEN_COOLDOWN_MS,
36
+ FED_DEFAULT_UNHEALTHY_THRESHOLD,
37
+ FED_DEFAULT_MAX_ACTIVE_NODES,
38
+ setFailureThreshold,
39
+ getFailureThreshold,
40
+ setHalfOpenCooldownMs,
41
+ getHalfOpenCooldownMs,
42
+ setUnhealthyThreshold,
43
+ getUnhealthyThreshold,
44
+ setMaxActiveNodes,
45
+ getMaxActiveNodes,
46
+ getActiveNodeCount,
47
+ registerNodeV2,
48
+ getNodeStatusV2,
49
+ setNodeStatusV2,
50
+ recordHealthCheckV2,
51
+ tripCircuit,
52
+ autoIsolateUnhealthyNodes,
53
+ getFederationHardeningStatsV2,
32
54
  } from "../lib/federation-hardening.js";
33
55
 
34
56
  function _dbFromCtx(cmd) {
@@ -423,5 +445,266 @@ export function registerFederationCommand(program) {
423
445
  );
424
446
  });
425
447
 
448
+ /* ── V2 (Phase 58) ──────────────────────────────── */
449
+
450
+ fed
451
+ .command("node-statuses-v2")
452
+ .description("List V2 node lifecycle statuses")
453
+ .option("--json", "JSON output")
454
+ .action((opts) => {
455
+ const statuses = Object.values(NODE_STATUS_V2);
456
+ if (opts.json) return console.log(JSON.stringify(statuses, null, 2));
457
+ for (const s of statuses) console.log(` ${s}`);
458
+ });
459
+
460
+ fed
461
+ .command("default-failure-threshold")
462
+ .description("Default circuit-breaker failure threshold")
463
+ .option("--json", "JSON output")
464
+ .action((opts) => {
465
+ if (opts.json)
466
+ return console.log(JSON.stringify(FED_DEFAULT_FAILURE_THRESHOLD));
467
+ console.log(FED_DEFAULT_FAILURE_THRESHOLD);
468
+ });
469
+
470
+ fed
471
+ .command("failure-threshold")
472
+ .description("Current failure threshold")
473
+ .option("--json", "JSON output")
474
+ .action((opts) => {
475
+ const n = getFailureThreshold();
476
+ if (opts.json) return console.log(JSON.stringify(n));
477
+ console.log(n);
478
+ });
479
+
480
+ fed
481
+ .command("set-failure-threshold <n>")
482
+ .description("Set failure threshold (positive integer)")
483
+ .option("--json", "JSON output")
484
+ .action((n, opts) => {
485
+ setFailureThreshold(Number(n));
486
+ const out = { failureThreshold: getFailureThreshold() };
487
+ if (opts.json) return console.log(JSON.stringify(out, null, 2));
488
+ console.log(`failureThreshold = ${out.failureThreshold}`);
489
+ });
490
+
491
+ fed
492
+ .command("default-half-open-cooldown-ms")
493
+ .description("Default half-open cooldown in ms")
494
+ .option("--json", "JSON output")
495
+ .action((opts) => {
496
+ if (opts.json)
497
+ return console.log(JSON.stringify(FED_DEFAULT_HALF_OPEN_COOLDOWN_MS));
498
+ console.log(FED_DEFAULT_HALF_OPEN_COOLDOWN_MS);
499
+ });
500
+
501
+ fed
502
+ .command("half-open-cooldown-ms")
503
+ .description("Current half-open cooldown in ms")
504
+ .option("--json", "JSON output")
505
+ .action((opts) => {
506
+ const n = getHalfOpenCooldownMs();
507
+ if (opts.json) return console.log(JSON.stringify(n));
508
+ console.log(n);
509
+ });
510
+
511
+ fed
512
+ .command("set-half-open-cooldown-ms <ms>")
513
+ .description("Set half-open cooldown (positive integer ms)")
514
+ .option("--json", "JSON output")
515
+ .action((ms, opts) => {
516
+ setHalfOpenCooldownMs(Number(ms));
517
+ const out = { halfOpenCooldownMs: getHalfOpenCooldownMs() };
518
+ if (opts.json) return console.log(JSON.stringify(out, null, 2));
519
+ console.log(`halfOpenCooldownMs = ${out.halfOpenCooldownMs}`);
520
+ });
521
+
522
+ fed
523
+ .command("default-unhealthy-threshold")
524
+ .description("Default consecutive-unhealthy isolation threshold")
525
+ .option("--json", "JSON output")
526
+ .action((opts) => {
527
+ if (opts.json)
528
+ return console.log(JSON.stringify(FED_DEFAULT_UNHEALTHY_THRESHOLD));
529
+ console.log(FED_DEFAULT_UNHEALTHY_THRESHOLD);
530
+ });
531
+
532
+ fed
533
+ .command("unhealthy-threshold")
534
+ .description("Current consecutive-unhealthy threshold")
535
+ .option("--json", "JSON output")
536
+ .action((opts) => {
537
+ const n = getUnhealthyThreshold();
538
+ if (opts.json) return console.log(JSON.stringify(n));
539
+ console.log(n);
540
+ });
541
+
542
+ fed
543
+ .command("set-unhealthy-threshold <n>")
544
+ .description("Set consecutive-unhealthy threshold")
545
+ .option("--json", "JSON output")
546
+ .action((n, opts) => {
547
+ setUnhealthyThreshold(Number(n));
548
+ const out = { unhealthyThreshold: getUnhealthyThreshold() };
549
+ if (opts.json) return console.log(JSON.stringify(out, null, 2));
550
+ console.log(`unhealthyThreshold = ${out.unhealthyThreshold}`);
551
+ });
552
+
553
+ fed
554
+ .command("default-max-active-nodes")
555
+ .description("Default max active nodes cap")
556
+ .option("--json", "JSON output")
557
+ .action((opts) => {
558
+ if (opts.json)
559
+ return console.log(JSON.stringify(FED_DEFAULT_MAX_ACTIVE_NODES));
560
+ console.log(FED_DEFAULT_MAX_ACTIVE_NODES);
561
+ });
562
+
563
+ fed
564
+ .command("max-active-nodes")
565
+ .description("Current max active nodes cap")
566
+ .option("--json", "JSON output")
567
+ .action((opts) => {
568
+ const n = getMaxActiveNodes();
569
+ if (opts.json) return console.log(JSON.stringify(n));
570
+ console.log(n);
571
+ });
572
+
573
+ fed
574
+ .command("active-node-count")
575
+ .description("Number of currently ACTIVE nodes")
576
+ .option("--json", "JSON output")
577
+ .action((opts) => {
578
+ const n = getActiveNodeCount();
579
+ if (opts.json) return console.log(JSON.stringify(n));
580
+ console.log(n);
581
+ });
582
+
583
+ fed
584
+ .command("set-max-active-nodes <n>")
585
+ .description("Set max active nodes cap")
586
+ .option("--json", "JSON output")
587
+ .action((n, opts) => {
588
+ setMaxActiveNodes(Number(n));
589
+ const out = { maxActiveNodes: getMaxActiveNodes() };
590
+ if (opts.json) return console.log(JSON.stringify(out, null, 2));
591
+ console.log(`maxActiveNodes = ${out.maxActiveNodes}`);
592
+ });
593
+
594
+ fed
595
+ .command("register-v2 <node-id>")
596
+ .description("Register a node with V2 lifecycle tracking")
597
+ .option("-m, --metadata <json>", "JSON metadata")
598
+ .option("--json", "JSON output")
599
+ .action((nodeId, opts) => {
600
+ const db = _dbFromCtx(fed);
601
+ const metadata = opts.metadata ? JSON.parse(opts.metadata) : undefined;
602
+ const r = registerNodeV2(db, { nodeId, metadata });
603
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
604
+ console.log(`Registered ${nodeId} (status: ${r.status})`);
605
+ });
606
+
607
+ fed
608
+ .command("node-status-v2 <node-id>")
609
+ .description("Get V2 node status")
610
+ .option("--json", "JSON output")
611
+ .action((nodeId, opts) => {
612
+ const r = getNodeStatusV2(nodeId);
613
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
614
+ if (!r) return console.log("(not found)");
615
+ console.log(`${nodeId}: ${r.status}${r.reason ? ` — ${r.reason}` : ""}`);
616
+ });
617
+
618
+ fed
619
+ .command("set-node-status-v2 <node-id> <status>")
620
+ .description("Transition node to a new status")
621
+ .option("-r, --reason <reason>")
622
+ .option("-m, --metadata <json>")
623
+ .option("--json", "JSON output")
624
+ .action((nodeId, status, opts) => {
625
+ const db = _dbFromCtx(fed);
626
+ const patch = {};
627
+ if (opts.reason !== undefined) patch.reason = opts.reason;
628
+ if (opts.metadata !== undefined)
629
+ patch.metadata = JSON.parse(opts.metadata);
630
+ const r = setNodeStatusV2(db, nodeId, status, patch);
631
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
632
+ console.log(`${nodeId} → ${r.status}`);
633
+ });
634
+
635
+ fed
636
+ .command("record-health-v2 <node-id>")
637
+ .description("Record a V2 health check (throws on invalid input)")
638
+ .option(
639
+ "-t, --type <checkType>",
640
+ "heartbeat|latency|success_rate|cpu_usage|memory_usage",
641
+ "heartbeat",
642
+ )
643
+ .option(
644
+ "-s, --status <status>",
645
+ "healthy|degraded|unhealthy|unknown",
646
+ "healthy",
647
+ )
648
+ .option("-m, --metrics <json>", "Optional metrics JSON")
649
+ .option("--json", "JSON output")
650
+ .action((nodeId, opts) => {
651
+ const db = _dbFromCtx(fed);
652
+ const metrics = opts.metrics ? JSON.parse(opts.metrics) : undefined;
653
+ const r = recordHealthCheckV2(db, {
654
+ nodeId,
655
+ checkType: opts.type,
656
+ status: opts.status,
657
+ metrics,
658
+ });
659
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
660
+ console.log(`recorded check ${r.checkId}`);
661
+ });
662
+
663
+ fed
664
+ .command("trip-circuit <node-id>")
665
+ .description("Force-trip a circuit breaker (closed/half_open → open)")
666
+ .option("--json", "JSON output")
667
+ .action((nodeId, opts) => {
668
+ const db = _dbFromCtx(fed);
669
+ const r = tripCircuit(db, nodeId);
670
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
671
+ console.log(`${nodeId} circuit → ${r.state}`);
672
+ });
673
+
674
+ fed
675
+ .command("auto-isolate-unhealthy")
676
+ .description(
677
+ "Bulk-isolate ACTIVE nodes with N consecutive UNHEALTHY checks",
678
+ )
679
+ .option("--json", "JSON output")
680
+ .action((opts) => {
681
+ const db = _dbFromCtx(fed);
682
+ const r = autoIsolateUnhealthyNodes(db);
683
+ if (opts.json) return console.log(JSON.stringify(r, null, 2));
684
+ console.log(`Isolated ${r.length} node(s)`);
685
+ for (const n of r) console.log(` ${n.node_id}`);
686
+ });
687
+
688
+ fed
689
+ .command("stats-v2")
690
+ .description("V2 federation hardening statistics")
691
+ .option("--json", "JSON output")
692
+ .action((opts) => {
693
+ const db = _dbFromCtx(fed);
694
+ const s = getFederationHardeningStatsV2(db);
695
+ if (opts.json) return console.log(JSON.stringify(s, null, 2));
696
+ console.log(
697
+ `Nodes: ${s.totalNodes} (active=${s.activeNodes}, isolated=${s.isolatedNodes})`,
698
+ );
699
+ console.log(`Circuits: ${s.totalCircuits}`);
700
+ for (const [state, count] of Object.entries(s.circuitsByState)) {
701
+ if (count > 0) console.log(` ${state.padEnd(10)} ${count}`);
702
+ }
703
+ console.log(`Health checks: ${s.totalHealthChecks}`);
704
+ console.log(
705
+ `config: failureThreshold=${s.failureThreshold} halfOpenCooldownMs=${s.halfOpenCooldownMs} unhealthyThreshold=${s.unhealthyThreshold} maxActiveNodes=${s.maxActiveNodes}`,
706
+ );
707
+ });
708
+
426
709
  program.addCommand(fed);
427
710
  }