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.
@@ -394,4 +394,329 @@ export function _resetState() {
394
394
  _services.clear();
395
395
  _invocations.clear();
396
396
  _seq = 0;
397
+ _maxConcurrentInvocationsPerService =
398
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
399
+ }
400
+
401
+ /* ═══════════════════════════════════════════════════════════════
402
+ * V2 (Phase 65) — Frozen enums + async invocation lifecycle +
403
+ * per-service concurrency cap + patch-merged setInvocationStatus +
404
+ * stats-v2. Strictly additive on top of the legacy surface above.
405
+ * ═══════════════════════════════════════════════════════════════ */
406
+
407
+ export const SERVICE_STATUS_V2 = Object.freeze({
408
+ DRAFT: "draft",
409
+ PUBLISHED: "published",
410
+ DEPRECATED: "deprecated",
411
+ SUSPENDED: "suspended",
412
+ });
413
+
414
+ export const INVOCATION_STATUS_V2 = Object.freeze({
415
+ PENDING: "pending",
416
+ RUNNING: "running",
417
+ SUCCESS: "success",
418
+ FAILED: "failed",
419
+ TIMEOUT: "timeout",
420
+ });
421
+
422
+ export const PRICING_MODEL_V2 = Object.freeze({
423
+ FREE: "free",
424
+ PAY_PER_CALL: "pay_per_call",
425
+ SUBSCRIPTION: "subscription",
426
+ TIERED: "tiered",
427
+ });
428
+
429
+ const DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE = 10;
430
+ let _maxConcurrentInvocationsPerService =
431
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
432
+ export const MARKETPLACE_DEFAULT_MAX_CONCURRENT_INVOCATIONS =
433
+ DEFAULT_MAX_CONCURRENT_INVOCATIONS_PER_SERVICE;
434
+
435
+ export function setMaxConcurrentInvocations(n) {
436
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
437
+ throw new Error("maxConcurrentInvocations must be a positive integer");
438
+ }
439
+ _maxConcurrentInvocationsPerService = Math.floor(n);
440
+ return _maxConcurrentInvocationsPerService;
441
+ }
442
+
443
+ export function getMaxConcurrentInvocations() {
444
+ return _maxConcurrentInvocationsPerService;
445
+ }
446
+
447
+ export function getActiveInvocationCount(serviceId) {
448
+ let count = 0;
449
+ for (const inv of _invocations.values()) {
450
+ if (
451
+ (serviceId == null || inv.serviceId === serviceId) &&
452
+ (inv.status === INVOCATION_STATUS_V2.PENDING ||
453
+ inv.status === INVOCATION_STATUS_V2.RUNNING)
454
+ ) {
455
+ count++;
456
+ }
457
+ }
458
+ return count;
459
+ }
460
+
461
+ // Invocation state machine:
462
+ // pending → { running, failed, timeout }
463
+ // running → { success, failed, timeout }
464
+ // success/failed/timeout are terminal.
465
+ const _invocationTerminal = new Set([
466
+ INVOCATION_STATUS_V2.SUCCESS,
467
+ INVOCATION_STATUS_V2.FAILED,
468
+ INVOCATION_STATUS_V2.TIMEOUT,
469
+ ]);
470
+ const _invocationAllowed = new Map([
471
+ [
472
+ INVOCATION_STATUS_V2.PENDING,
473
+ new Set([
474
+ INVOCATION_STATUS_V2.RUNNING,
475
+ INVOCATION_STATUS_V2.FAILED,
476
+ INVOCATION_STATUS_V2.TIMEOUT,
477
+ ]),
478
+ ],
479
+ [
480
+ INVOCATION_STATUS_V2.RUNNING,
481
+ new Set([
482
+ INVOCATION_STATUS_V2.SUCCESS,
483
+ INVOCATION_STATUS_V2.FAILED,
484
+ INVOCATION_STATUS_V2.TIMEOUT,
485
+ ]),
486
+ ],
487
+ [INVOCATION_STATUS_V2.SUCCESS, new Set([])],
488
+ [INVOCATION_STATUS_V2.FAILED, new Set([])],
489
+ [INVOCATION_STATUS_V2.TIMEOUT, new Set([])],
490
+ ]);
491
+
492
+ /**
493
+ * beginInvocationV2 — creates a PENDING invocation row (no output/duration).
494
+ * Caller drives the transition via startInvocation (→ RUNNING),
495
+ * completeInvocation (→ SUCCESS), failInvocation / timeoutInvocation,
496
+ * or the generic setInvocationStatus.
497
+ */
498
+ export function beginInvocationV2(db, config = {}) {
499
+ const serviceId = String(config.serviceId || "").trim();
500
+ if (!serviceId) throw new Error("serviceId is required");
501
+ const service = _mustGetService(serviceId);
502
+ if (service.status !== SERVICE_STATUS_V2.PUBLISHED) {
503
+ throw new Error(
504
+ `Cannot invoke non-published service (status=${service.status})`,
505
+ );
506
+ }
507
+
508
+ const activeCount = getActiveInvocationCount(serviceId);
509
+ if (activeCount >= _maxConcurrentInvocationsPerService) {
510
+ throw new Error(
511
+ `Max concurrent invocations reached: ${activeCount}/${_maxConcurrentInvocationsPerService}`,
512
+ );
513
+ }
514
+
515
+ const callerId = config.callerId ? String(config.callerId).trim() : null;
516
+ const input = config.input ?? null;
517
+ const now = Number(config.now ?? Date.now());
518
+ const id = config.id || crypto.randomUUID();
519
+
520
+ const invocation = {
521
+ id,
522
+ serviceId,
523
+ callerId,
524
+ input,
525
+ output: null,
526
+ status: INVOCATION_STATUS_V2.PENDING,
527
+ durationMs: null,
528
+ error: null,
529
+ startedAt: null,
530
+ completedAt: null,
531
+ createdAt: now,
532
+ _seq: ++_seq,
533
+ };
534
+ _invocations.set(id, invocation);
535
+
536
+ if (db) {
537
+ db.prepare(
538
+ `INSERT INTO skill_invocations (id, service_id, caller_id, input, output, status, duration_ms, error, created_at)
539
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
540
+ ).run(
541
+ id,
542
+ serviceId,
543
+ callerId,
544
+ input == null ? null : JSON.stringify(input),
545
+ null,
546
+ INVOCATION_STATUS_V2.PENDING,
547
+ null,
548
+ null,
549
+ now,
550
+ );
551
+ }
552
+
553
+ return _strip(invocation);
554
+ }
555
+
556
+ function _mustGetInvocation(invocationId) {
557
+ const inv = _invocations.get(invocationId);
558
+ if (!inv) throw new Error(`Invocation not found: ${invocationId}`);
559
+ return inv;
560
+ }
561
+
562
+ function _persistInvocation(db, invocationId, fields) {
563
+ if (!db) return;
564
+ const setClauses = Object.keys(fields)
565
+ .map((k) => `${k} = ?`)
566
+ .join(", ");
567
+ const values = Object.values(fields).map((v) =>
568
+ v && typeof v === "object" ? JSON.stringify(v) : v,
569
+ );
570
+ db.prepare(`UPDATE skill_invocations SET ${setClauses} WHERE id = ?`).run(
571
+ ...values,
572
+ invocationId,
573
+ );
574
+ }
575
+
576
+ export function startInvocation(db, invocationId, opts = {}) {
577
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.RUNNING, {
578
+ startedAt: Number(opts.now ?? Date.now()),
579
+ });
580
+ }
581
+
582
+ export function completeInvocationV2(db, invocationId, opts = {}) {
583
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.SUCCESS, {
584
+ output: opts.output ?? null,
585
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
586
+ });
587
+ }
588
+
589
+ export function failInvocationV2(db, invocationId, errorMessage, opts = {}) {
590
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.FAILED, {
591
+ error: errorMessage ? String(errorMessage) : null,
592
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
593
+ });
594
+ }
595
+
596
+ export function timeoutInvocationV2(db, invocationId, opts = {}) {
597
+ return setInvocationStatus(db, invocationId, INVOCATION_STATUS_V2.TIMEOUT, {
598
+ error: opts.error ? String(opts.error) : "timeout",
599
+ durationMs: opts.durationMs == null ? null : Number(opts.durationMs),
600
+ });
601
+ }
602
+
603
+ export function setInvocationStatus(db, invocationId, newStatus, patch = {}) {
604
+ const inv = _mustGetInvocation(invocationId);
605
+
606
+ if (!Object.values(INVOCATION_STATUS_V2).includes(newStatus)) {
607
+ throw new Error(`Unknown invocation status: ${newStatus}`);
608
+ }
609
+
610
+ const allowed = _invocationAllowed.get(inv.status);
611
+ if (!allowed || !allowed.has(newStatus)) {
612
+ throw new Error(
613
+ `Invalid invocation status transition: ${inv.status} → ${newStatus}`,
614
+ );
615
+ }
616
+
617
+ const now = Date.now();
618
+ inv.status = newStatus;
619
+
620
+ const dbFields = { status: newStatus };
621
+
622
+ if (patch.output !== undefined) {
623
+ inv.output = patch.output;
624
+ dbFields.output =
625
+ patch.output == null ? null : JSON.stringify(patch.output);
626
+ }
627
+ if (patch.error !== undefined) {
628
+ inv.error = patch.error;
629
+ dbFields.error = patch.error;
630
+ }
631
+ if (patch.durationMs !== undefined) {
632
+ const d = patch.durationMs == null ? null : Number(patch.durationMs);
633
+ if (d != null && (Number.isNaN(d) || d < 0)) {
634
+ throw new Error(`Invalid durationMs: ${patch.durationMs}`);
635
+ }
636
+ inv.durationMs = d;
637
+ dbFields.duration_ms = d;
638
+ }
639
+ if (patch.startedAt !== undefined) {
640
+ inv.startedAt = patch.startedAt;
641
+ }
642
+
643
+ if (_invocationTerminal.has(newStatus) && inv.completedAt == null) {
644
+ inv.completedAt = now;
645
+ // Bump the service's invocation counter once on terminal.
646
+ const service = _services.get(inv.serviceId);
647
+ if (service) {
648
+ service.invocationCount = (service.invocationCount || 0) + 1;
649
+ service.updatedAt = now;
650
+ _persistService(db, inv.serviceId, {
651
+ invocation_count: service.invocationCount,
652
+ updated_at: now,
653
+ });
654
+ }
655
+ }
656
+
657
+ _persistInvocation(db, invocationId, dbFields);
658
+
659
+ return _strip(inv);
660
+ }
661
+
662
+ export function getMarketplaceStatsV2() {
663
+ const services = [..._services.values()];
664
+ const invocations = [..._invocations.values()];
665
+
666
+ const servicesByStatus = {};
667
+ for (const s of Object.values(SERVICE_STATUS_V2)) servicesByStatus[s] = 0;
668
+ for (const s of services)
669
+ servicesByStatus[s.status] = (servicesByStatus[s.status] || 0) + 1;
670
+
671
+ const invocationsByStatus = {};
672
+ for (const s of Object.values(INVOCATION_STATUS_V2))
673
+ invocationsByStatus[s] = 0;
674
+ for (const inv of invocations)
675
+ invocationsByStatus[inv.status] =
676
+ (invocationsByStatus[inv.status] || 0) + 1;
677
+
678
+ const servicesByPricing = {};
679
+ for (const p of Object.values(PRICING_MODEL_V2)) servicesByPricing[p] = 0;
680
+ for (const s of services) {
681
+ const model =
682
+ s.pricing && typeof s.pricing === "object" && s.pricing.model
683
+ ? String(s.pricing.model)
684
+ : PRICING_MODEL_V2.FREE;
685
+ if (servicesByPricing[model] == null) servicesByPricing[model] = 0;
686
+ servicesByPricing[model]++;
687
+ }
688
+
689
+ let totalDuration = 0;
690
+ let durationSamples = 0;
691
+ for (const inv of invocations) {
692
+ if (inv.durationMs != null && inv.status === INVOCATION_STATUS_V2.SUCCESS) {
693
+ totalDuration += inv.durationMs;
694
+ durationSamples++;
695
+ }
696
+ }
697
+ const avgDurationMs =
698
+ durationSamples > 0
699
+ ? Number((totalDuration / durationSamples).toFixed(1))
700
+ : 0;
701
+ const successRate =
702
+ invocations.length > 0
703
+ ? Number(
704
+ (
705
+ invocationsByStatus[INVOCATION_STATUS_V2.SUCCESS] /
706
+ invocations.length
707
+ ).toFixed(3),
708
+ )
709
+ : 0;
710
+
711
+ return {
712
+ totalServices: services.length,
713
+ totalInvocations: invocations.length,
714
+ activeInvocations: getActiveInvocationCount(),
715
+ maxConcurrentInvocations: _maxConcurrentInvocationsPerService,
716
+ servicesByStatus,
717
+ invocationsByStatus,
718
+ servicesByPricing,
719
+ avgDurationMs,
720
+ successRate,
721
+ };
397
722
  }
@@ -481,4 +481,279 @@ export function _resetState() {
481
481
  _metrics.clear();
482
482
  _violations.clear();
483
483
  _seq = 0;
484
+ _maxActiveSlasPerOrg = DEFAULT_MAX_ACTIVE_SLAS_PER_ORG;
485
+ }
486
+
487
+ /* ═══════════════════════════════════════════════════════════════
488
+ * V2 (Phase 61) — Frozen enums + contract/violation state
489
+ * machines + active-per-org cap + auto-expire + stats-v2.
490
+ * Strictly additive on top of the legacy surface above.
491
+ * ═══════════════════════════════════════════════════════════════ */
492
+
493
+ export const SLA_STATUS_V2 = Object.freeze({
494
+ ACTIVE: "active",
495
+ EXPIRED: "expired",
496
+ TERMINATED: "terminated",
497
+ });
498
+
499
+ export const SLA_TIER_V2 = Object.freeze({
500
+ GOLD: "gold",
501
+ SILVER: "silver",
502
+ BRONZE: "bronze",
503
+ });
504
+
505
+ export const SLA_TERM_V2 = Object.freeze({
506
+ AVAILABILITY: "availability",
507
+ RESPONSE_TIME: "response_time",
508
+ THROUGHPUT: "throughput",
509
+ ERROR_RATE: "error_rate",
510
+ });
511
+
512
+ export const VIOLATION_SEVERITY_V2 = Object.freeze({
513
+ MINOR: "minor",
514
+ MODERATE: "moderate",
515
+ MAJOR: "major",
516
+ CRITICAL: "critical",
517
+ });
518
+
519
+ export const VIOLATION_STATUS_V2 = Object.freeze({
520
+ OPEN: "open",
521
+ ACKNOWLEDGED: "acknowledged",
522
+ RESOLVED: "resolved",
523
+ WAIVED: "waived",
524
+ });
525
+
526
+ const DEFAULT_MAX_ACTIVE_SLAS_PER_ORG = 1;
527
+ let _maxActiveSlasPerOrg = DEFAULT_MAX_ACTIVE_SLAS_PER_ORG;
528
+ export const SLA_DEFAULT_MAX_ACTIVE_PER_ORG = DEFAULT_MAX_ACTIVE_SLAS_PER_ORG;
529
+
530
+ export function setMaxActiveSlasPerOrg(n) {
531
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1) {
532
+ throw new Error("maxActiveSlasPerOrg must be a positive integer");
533
+ }
534
+ _maxActiveSlasPerOrg = Math.floor(n);
535
+ return _maxActiveSlasPerOrg;
536
+ }
537
+
538
+ export function getMaxActiveSlasPerOrg() {
539
+ return _maxActiveSlasPerOrg;
540
+ }
541
+
542
+ // Contract state machine: active → { expired, terminated }; both terminal.
543
+ const _contractTerminal = new Set([
544
+ SLA_STATUS_V2.EXPIRED,
545
+ SLA_STATUS_V2.TERMINATED,
546
+ ]);
547
+ const _contractAllowed = new Map([
548
+ [
549
+ SLA_STATUS_V2.ACTIVE,
550
+ new Set([SLA_STATUS_V2.EXPIRED, SLA_STATUS_V2.TERMINATED]),
551
+ ],
552
+ [SLA_STATUS_V2.EXPIRED, new Set([])],
553
+ [SLA_STATUS_V2.TERMINATED, new Set([])],
554
+ ]);
555
+
556
+ // Violation state machine: open → { acknowledged, resolved, waived };
557
+ // acknowledged → { resolved, waived }; resolved/waived terminal.
558
+ const _violationTerminal = new Set([
559
+ VIOLATION_STATUS_V2.RESOLVED,
560
+ VIOLATION_STATUS_V2.WAIVED,
561
+ ]);
562
+ const _violationAllowed = new Map([
563
+ [
564
+ VIOLATION_STATUS_V2.OPEN,
565
+ new Set([
566
+ VIOLATION_STATUS_V2.ACKNOWLEDGED,
567
+ VIOLATION_STATUS_V2.RESOLVED,
568
+ VIOLATION_STATUS_V2.WAIVED,
569
+ ]),
570
+ ],
571
+ [
572
+ VIOLATION_STATUS_V2.ACKNOWLEDGED,
573
+ new Set([VIOLATION_STATUS_V2.RESOLVED, VIOLATION_STATUS_V2.WAIVED]),
574
+ ],
575
+ [VIOLATION_STATUS_V2.RESOLVED, new Set([])],
576
+ [VIOLATION_STATUS_V2.WAIVED, new Set([])],
577
+ ]);
578
+
579
+ export function getActiveSlaCountForOrg(orgId) {
580
+ let count = 0;
581
+ for (const c of _contracts.values()) {
582
+ if (c.orgId === orgId && c.status === SLA_STATUS_V2.ACTIVE) count++;
583
+ }
584
+ return count;
585
+ }
586
+
587
+ /**
588
+ * createSLAV2 — like createSLA but enforces per-org active-contract cap
589
+ * and rejects unknown tier/status at the boundary. Augments stored
590
+ * contract with in-memory V2 fields (violationStatus doesn't apply here;
591
+ * see recordViolation*).
592
+ */
593
+ export function createSLAV2(db, config = {}) {
594
+ const orgId = config.orgId;
595
+ if (!orgId) throw new Error("orgId is required");
596
+
597
+ const activeCount = getActiveSlaCountForOrg(orgId);
598
+ if (activeCount >= _maxActiveSlasPerOrg) {
599
+ throw new Error(
600
+ `Max active SLAs per org reached: ${activeCount}/${_maxActiveSlasPerOrg}`,
601
+ );
602
+ }
603
+
604
+ return createSLA(db, config);
605
+ }
606
+
607
+ export function setSLAStatus(db, slaId, newStatus) {
608
+ const contract = _contracts.get(slaId);
609
+ if (!contract) throw new Error(`SLA not found: ${slaId}`);
610
+
611
+ if (!Object.values(SLA_STATUS_V2).includes(newStatus)) {
612
+ throw new Error(`Unknown SLA status: ${newStatus}`);
613
+ }
614
+
615
+ const allowed = _contractAllowed.get(contract.status);
616
+ if (!allowed || !allowed.has(newStatus)) {
617
+ throw new Error(
618
+ `Invalid SLA status transition: ${contract.status} → ${newStatus}`,
619
+ );
620
+ }
621
+
622
+ contract.status = newStatus;
623
+ contract.updatedAt = Date.now();
624
+ db.prepare(
625
+ `UPDATE sla_contracts SET status = ?, updated_at = ? WHERE sla_id = ?`,
626
+ ).run(contract.status, contract.updatedAt, slaId);
627
+
628
+ const { _seq: _omit, ...rest } = contract;
629
+ void _omit;
630
+ return rest;
631
+ }
632
+
633
+ export function expireSLA(db, slaId) {
634
+ return setSLAStatus(db, slaId, SLA_STATUS_V2.EXPIRED);
635
+ }
636
+
637
+ /**
638
+ * autoExpireSLAs — bulk-flip ACTIVE contracts whose endDate < now to
639
+ * EXPIRED. Returns the list of flipped contracts.
640
+ */
641
+ export function autoExpireSLAs(db, nowMs = Date.now()) {
642
+ const flipped = [];
643
+ for (const contract of _contracts.values()) {
644
+ if (contract.status === SLA_STATUS_V2.ACTIVE && contract.endDate < nowMs) {
645
+ contract.status = SLA_STATUS_V2.EXPIRED;
646
+ contract.updatedAt = nowMs;
647
+ db.prepare(
648
+ `UPDATE sla_contracts SET status = ?, updated_at = ? WHERE sla_id = ?`,
649
+ ).run(contract.status, contract.updatedAt, contract.slaId);
650
+ const { _seq: _omit, ...rest } = contract;
651
+ void _omit;
652
+ flipped.push(rest);
653
+ }
654
+ }
655
+ return flipped;
656
+ }
657
+
658
+ export function setViolationStatus(db, violationId, newStatus, patch = {}) {
659
+ const violation = _violations.get(violationId);
660
+ if (!violation) throw new Error(`Violation not found: ${violationId}`);
661
+
662
+ if (!Object.values(VIOLATION_STATUS_V2).includes(newStatus)) {
663
+ throw new Error(`Unknown violation status: ${newStatus}`);
664
+ }
665
+
666
+ const current = violation.v2Status || VIOLATION_STATUS_V2.OPEN;
667
+ const allowed = _violationAllowed.get(current);
668
+ if (!allowed || !allowed.has(newStatus)) {
669
+ throw new Error(
670
+ `Invalid violation status transition: ${current} → ${newStatus}`,
671
+ );
672
+ }
673
+
674
+ violation.v2Status = newStatus;
675
+ if (typeof patch.note === "string") {
676
+ violation.note = patch.note;
677
+ }
678
+ if (_violationTerminal.has(newStatus)) {
679
+ violation.resolvedAt = Date.now();
680
+ db.prepare(
681
+ `UPDATE sla_violations SET resolved_at = ? WHERE violation_id = ?`,
682
+ ).run(violation.resolvedAt, violationId);
683
+ }
684
+
685
+ return { ...violation };
686
+ }
687
+
688
+ export function acknowledgeViolation(db, violationId, note) {
689
+ return setViolationStatus(db, violationId, VIOLATION_STATUS_V2.ACKNOWLEDGED, {
690
+ note,
691
+ });
692
+ }
693
+
694
+ export function resolveViolation(db, violationId, note) {
695
+ return setViolationStatus(db, violationId, VIOLATION_STATUS_V2.RESOLVED, {
696
+ note,
697
+ });
698
+ }
699
+
700
+ export function waiveViolation(db, violationId, note) {
701
+ return setViolationStatus(db, violationId, VIOLATION_STATUS_V2.WAIVED, {
702
+ note,
703
+ });
704
+ }
705
+
706
+ export function getSLAStatsV2() {
707
+ const contracts = [..._contracts.values()];
708
+ const violations = [..._violations.values()];
709
+
710
+ const byStatus = {};
711
+ for (const s of Object.values(SLA_STATUS_V2)) byStatus[s] = 0;
712
+ for (const c of contracts) byStatus[c.status] = (byStatus[c.status] || 0) + 1;
713
+
714
+ const byTier = {};
715
+ for (const t of Object.values(SLA_TIER_V2)) byTier[t] = 0;
716
+ for (const c of contracts) byTier[c.tier] = (byTier[c.tier] || 0) + 1;
717
+
718
+ const bySeverity = {};
719
+ for (const s of Object.values(VIOLATION_SEVERITY_V2)) bySeverity[s] = 0;
720
+ for (const v of violations)
721
+ bySeverity[v.severity] = (bySeverity[v.severity] || 0) + 1;
722
+
723
+ const byTerm = {};
724
+ for (const t of Object.values(SLA_TERM_V2)) byTerm[t] = 0;
725
+ for (const v of violations) byTerm[v.term] = (byTerm[v.term] || 0) + 1;
726
+
727
+ const byViolationStatus = {};
728
+ for (const s of Object.values(VIOLATION_STATUS_V2)) byViolationStatus[s] = 0;
729
+ for (const v of violations) {
730
+ const s = v.v2Status || VIOLATION_STATUS_V2.OPEN;
731
+ byViolationStatus[s] = (byViolationStatus[s] || 0) + 1;
732
+ }
733
+
734
+ let totalCompensation = 0;
735
+ for (const v of violations) {
736
+ if (v.compensationAmount) totalCompensation += v.compensationAmount;
737
+ }
738
+
739
+ const activeOrgs = new Set();
740
+ for (const c of contracts) {
741
+ if (c.status === SLA_STATUS_V2.ACTIVE) activeOrgs.add(c.orgId);
742
+ }
743
+
744
+ return {
745
+ totalContracts: contracts.length,
746
+ activeContracts: byStatus[SLA_STATUS_V2.ACTIVE] || 0,
747
+ activeOrgs: activeOrgs.size,
748
+ maxActiveSlasPerOrg: _maxActiveSlasPerOrg,
749
+ byStatus,
750
+ byTier,
751
+ violations: {
752
+ total: violations.length,
753
+ byTerm,
754
+ bySeverity,
755
+ byStatus: byViolationStatus,
756
+ totalCompensation: Number(totalCompensation.toFixed(4)),
757
+ },
758
+ };
484
759
  }