observability-toolkit 1.8.5 → 2.1.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/README.md +167 -281
- package/dist/__tests__/find-constant-dedup.test.d.ts +11 -0
- package/dist/__tests__/find-constant-dedup.test.d.ts.map +1 -0
- package/dist/__tests__/find-constant-dedup.test.js +132 -0
- package/dist/__tests__/find-constant-dedup.test.js.map +1 -0
- package/dist/backends/backend-schemas.d.ts +309 -0
- package/dist/backends/backend-schemas.d.ts.map +1 -0
- package/dist/backends/backend-schemas.js +215 -0
- package/dist/backends/backend-schemas.js.map +1 -0
- package/dist/backends/cloud.d.ts +46 -0
- package/dist/backends/cloud.d.ts.map +1 -0
- package/dist/backends/cloud.js +520 -0
- package/dist/backends/cloud.js.map +1 -0
- package/dist/backends/cloud.test.d.ts +2 -0
- package/dist/backends/cloud.test.d.ts.map +1 -0
- package/dist/backends/cloud.test.js +436 -0
- package/dist/backends/cloud.test.js.map +1 -0
- package/dist/backends/index.d.ts +672 -236
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +334 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/index.test.js +606 -31
- package/dist/backends/index.test.js.map +1 -1
- package/dist/backends/local-jsonl-boolean-search.test.js +8 -7
- package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
- package/dist/backends/local-jsonl-cache.test.js +33 -31
- package/dist/backends/local-jsonl-cache.test.js.map +1 -1
- package/dist/backends/local-jsonl-circuit-breaker.test.js +9 -7
- package/dist/backends/local-jsonl-circuit-breaker.test.js.map +1 -1
- package/dist/backends/local-jsonl-export.test.js +73 -58
- package/dist/backends/local-jsonl-export.test.js.map +1 -1
- package/dist/backends/local-jsonl-index.test.js +52 -50
- package/dist/backends/local-jsonl-index.test.js.map +1 -1
- package/dist/backends/local-jsonl-logs.test.js +47 -31
- package/dist/backends/local-jsonl-logs.test.js.map +1 -1
- package/dist/backends/local-jsonl-metrics.test.js +85 -82
- package/dist/backends/local-jsonl-metrics.test.js.map +1 -1
- package/dist/backends/local-jsonl-otlp-unwrap.test.d.ts +2 -0
- package/dist/backends/local-jsonl-otlp-unwrap.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-otlp-unwrap.test.js +602 -0
- package/dist/backends/local-jsonl-otlp-unwrap.test.js.map +1 -0
- package/dist/backends/local-jsonl-traces.test.js +161 -147
- package/dist/backends/local-jsonl-traces.test.js.map +1 -1
- package/dist/backends/local-jsonl.d.ts +64 -5
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +1821 -612
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/shared.d.ts +9 -0
- package/dist/backends/shared.d.ts.map +1 -0
- package/dist/backends/shared.js +9 -0
- package/dist/backends/shared.js.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/logs/v1/logs_service_pb.d.ts +40 -0
- package/dist/generated/opentelemetry/proto/collector/logs/v1/logs_service_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/logs/v1/logs_service_pb.js +27 -0
- package/dist/generated/opentelemetry/proto/collector/logs/v1/logs_service_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.d.ts +106 -0
- package/dist/generated/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.js +43 -0
- package/dist/generated/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.d.ts +111 -0
- package/dist/generated/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.js +42 -0
- package/dist/generated/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/trace/v1/trace_service_pb.d.ts +106 -0
- package/dist/generated/opentelemetry/proto/collector/trace/v1/trace_service_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/collector/trace/v1/trace_service_pb.js +43 -0
- package/dist/generated/opentelemetry/proto/collector/trace/v1/trace_service_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/common/v1/common_pb.d.ts +243 -0
- package/dist/generated/opentelemetry/proto/common/v1/common_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/common/v1/common_pb.js +49 -0
- package/dist/generated/opentelemetry/proto/common/v1/common_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/logs/v1/logs_pb.d.ts +90 -0
- package/dist/generated/opentelemetry/proto/logs/v1/logs_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/logs/v1/logs_pb.js +66 -0
- package/dist/generated/opentelemetry/proto/logs/v1/logs_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/metrics/v1/metrics_pb.d.ts +1134 -0
- package/dist/generated/opentelemetry/proto/metrics/v1/metrics_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/metrics/v1/metrics_pb.js +223 -0
- package/dist/generated/opentelemetry/proto/metrics/v1/metrics_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/profiles/v1development/profiles_pb.d.ts +678 -0
- package/dist/generated/opentelemetry/proto/profiles/v1development/profiles_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/profiles/v1development/profiles_pb.js +107 -0
- package/dist/generated/opentelemetry/proto/profiles/v1development/profiles_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/resource/v1/resource_pb.d.ts +46 -0
- package/dist/generated/opentelemetry/proto/resource/v1/resource_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/resource/v1/resource_pb.js +25 -0
- package/dist/generated/opentelemetry/proto/resource/v1/resource_pb.js.map +1 -0
- package/dist/generated/opentelemetry/proto/trace/v1/trace_pb.d.ts +569 -0
- package/dist/generated/opentelemetry/proto/trace/v1/trace_pb.d.ts.map +1 -0
- package/dist/generated/opentelemetry/proto/trace/v1/trace_pb.js +195 -0
- package/dist/generated/opentelemetry/proto/trace/v1/trace_pb.js.map +1 -0
- package/dist/lib/agent-judge/agent-as-judge.d.ts +157 -0
- package/dist/lib/agent-judge/agent-as-judge.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-as-judge.js +137 -0
- package/dist/lib/agent-judge/agent-as-judge.js.map +1 -0
- package/dist/lib/agent-judge/agent-as-judge.test.d.ts +5 -0
- package/dist/lib/agent-judge/agent-as-judge.test.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-as-judge.test.js +839 -0
- package/dist/lib/agent-judge/agent-as-judge.test.js.map +1 -0
- package/dist/lib/agent-judge/agent-eval-metrics.d.ts +293 -0
- package/dist/lib/agent-judge/agent-eval-metrics.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-eval-metrics.js +715 -0
- package/dist/lib/agent-judge/agent-eval-metrics.js.map +1 -0
- package/dist/lib/agent-judge/agent-eval-metrics.test.d.ts +5 -0
- package/dist/lib/agent-judge/agent-eval-metrics.test.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-eval-metrics.test.js +676 -0
- package/dist/lib/agent-judge/agent-eval-metrics.test.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-classes.d.ts +95 -0
- package/dist/lib/agent-judge/agent-judge-classes.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-classes.js +222 -0
- package/dist/lib/agent-judge/agent-judge-classes.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-classes.test.d.ts +6 -0
- package/dist/lib/agent-judge/agent-judge-classes.test.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-classes.test.js +271 -0
- package/dist/lib/agent-judge/agent-judge-classes.test.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-consensus.d.ts +58 -0
- package/dist/lib/agent-judge/agent-judge-consensus.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-consensus.js +149 -0
- package/dist/lib/agent-judge/agent-judge-consensus.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-consensus.test.d.ts +2 -0
- package/dist/lib/agent-judge/agent-judge-consensus.test.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-consensus.test.js +170 -0
- package/dist/lib/agent-judge/agent-judge-consensus.test.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-verification.d.ts +89 -0
- package/dist/lib/agent-judge/agent-judge-verification.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-verification.js +235 -0
- package/dist/lib/agent-judge/agent-judge-verification.js.map +1 -0
- package/dist/lib/agent-judge/agent-judge-verification.test.d.ts +5 -0
- package/dist/lib/agent-judge/agent-judge-verification.test.d.ts.map +1 -0
- package/dist/lib/agent-judge/agent-judge-verification.test.js +399 -0
- package/dist/lib/agent-judge/agent-judge-verification.test.js.map +1 -0
- package/dist/lib/audit/agent-auditor-scoring.d.ts +167 -0
- package/dist/lib/audit/agent-auditor-scoring.d.ts.map +1 -0
- package/dist/lib/audit/agent-auditor-scoring.js +338 -0
- package/dist/lib/audit/agent-auditor-scoring.js.map +1 -0
- package/dist/lib/audit/agent-auditor-scoring.test.d.ts +2 -0
- package/dist/lib/audit/agent-auditor-scoring.test.d.ts.map +1 -0
- package/dist/lib/audit/agent-auditor-scoring.test.js +576 -0
- package/dist/lib/audit/agent-auditor-scoring.test.js.map +1 -0
- package/dist/lib/audit/audit-record.d.ts +139 -0
- package/dist/lib/audit/audit-record.d.ts.map +1 -0
- package/dist/lib/audit/audit-record.js +288 -0
- package/dist/lib/audit/audit-record.js.map +1 -0
- package/dist/lib/audit/audit-record.test.d.ts +5 -0
- package/dist/lib/audit/audit-record.test.d.ts.map +1 -0
- package/dist/lib/audit/audit-record.test.js +258 -0
- package/dist/lib/audit/audit-record.test.js.map +1 -0
- package/dist/lib/audit/audit-scoring-constants.d.ts +57 -0
- package/dist/lib/audit/audit-scoring-constants.d.ts.map +1 -0
- package/dist/lib/audit/audit-scoring-constants.js +59 -0
- package/dist/lib/audit/audit-scoring-constants.js.map +1 -0
- package/dist/lib/audit/compliance-report.d.ts +125 -0
- package/dist/lib/audit/compliance-report.d.ts.map +1 -0
- package/dist/lib/audit/compliance-report.js +205 -0
- package/dist/lib/audit/compliance-report.js.map +1 -0
- package/dist/lib/audit/compliance-report.test.d.ts +5 -0
- package/dist/lib/audit/compliance-report.test.d.ts.map +1 -0
- package/dist/lib/audit/compliance-report.test.js +290 -0
- package/dist/lib/audit/compliance-report.test.js.map +1 -0
- package/dist/lib/audit/retention-guard.d.ts +41 -0
- package/dist/lib/audit/retention-guard.d.ts.map +1 -0
- package/dist/lib/audit/retention-guard.js +103 -0
- package/dist/lib/audit/retention-guard.js.map +1 -0
- package/dist/lib/audit/retention-guard.test.d.ts +5 -0
- package/dist/lib/audit/retention-guard.test.d.ts.map +1 -0
- package/dist/lib/audit/retention-guard.test.js +109 -0
- package/dist/lib/audit/retention-guard.test.js.map +1 -0
- package/dist/lib/audit/skill-auditor-scoring.d.ts +69 -0
- package/dist/lib/audit/skill-auditor-scoring.d.ts.map +1 -0
- package/dist/lib/audit/skill-auditor-scoring.js +149 -0
- package/dist/lib/audit/skill-auditor-scoring.js.map +1 -0
- package/dist/lib/audit/skill-auditor-scoring.test.d.ts +2 -0
- package/dist/lib/audit/skill-auditor-scoring.test.d.ts.map +1 -0
- package/dist/lib/audit/skill-auditor-scoring.test.js +369 -0
- package/dist/lib/audit/skill-auditor-scoring.test.js.map +1 -0
- package/dist/lib/audit/verification-events.d.ts +119 -0
- package/dist/lib/audit/verification-events.d.ts.map +1 -0
- package/dist/lib/audit/verification-events.js +175 -0
- package/dist/lib/audit/verification-events.js.map +1 -0
- package/dist/lib/audit/verification-events.test.d.ts +5 -0
- package/dist/lib/audit/verification-events.test.d.ts.map +1 -0
- package/dist/lib/audit/verification-events.test.js +197 -0
- package/dist/lib/audit/verification-events.test.js.map +1 -0
- package/dist/lib/core/constants-models.d.ts +90 -0
- package/dist/lib/core/constants-models.d.ts.map +1 -0
- package/dist/lib/core/constants-models.js +208 -0
- package/dist/lib/core/constants-models.js.map +1 -0
- package/dist/lib/core/constants-otel.d.ts +68 -0
- package/dist/lib/core/constants-otel.d.ts.map +1 -0
- package/dist/lib/core/constants-otel.js +128 -0
- package/dist/lib/core/constants-otel.js.map +1 -0
- package/dist/lib/core/constants-symlink.test.d.ts.map +1 -0
- package/dist/lib/{constants-symlink.test.js → core/constants-symlink.test.js} +25 -24
- package/dist/lib/core/constants-symlink.test.js.map +1 -0
- package/dist/lib/core/constants-telemetry.d.ts +21 -0
- package/dist/lib/core/constants-telemetry.d.ts.map +1 -0
- package/dist/lib/core/constants-telemetry.js +162 -0
- package/dist/lib/core/constants-telemetry.js.map +1 -0
- package/dist/lib/core/constants.d.ts +152 -0
- package/dist/lib/core/constants.d.ts.map +1 -0
- package/dist/lib/core/constants.js +223 -0
- package/dist/lib/core/constants.js.map +1 -0
- package/dist/lib/core/constants.test.d.ts.map +1 -0
- package/dist/lib/{constants.test.js → core/constants.test.js} +198 -82
- package/dist/lib/core/constants.test.js.map +1 -0
- package/dist/lib/core/doc-sync.test.d.ts +9 -0
- package/dist/lib/core/doc-sync.test.d.ts.map +1 -0
- package/dist/lib/core/doc-sync.test.js +159 -0
- package/dist/lib/core/doc-sync.test.js.map +1 -0
- package/dist/lib/core/edge-cases.test.d.ts.map +1 -0
- package/dist/lib/{edge-cases.test.js → core/edge-cases.test.js} +76 -73
- package/dist/lib/core/edge-cases.test.js.map +1 -0
- package/dist/lib/{file-utils.d.ts → core/file-utils.d.ts} +63 -8
- package/dist/lib/core/file-utils.d.ts.map +1 -0
- package/dist/lib/{file-utils.js → core/file-utils.js} +186 -93
- package/dist/lib/core/file-utils.js.map +1 -0
- package/dist/lib/core/file-utils.test-constants.d.ts +38 -0
- package/dist/lib/core/file-utils.test-constants.d.ts.map +1 -0
- package/dist/lib/core/file-utils.test-constants.js +40 -0
- package/dist/lib/core/file-utils.test-constants.js.map +1 -0
- package/dist/lib/core/file-utils.test.d.ts.map +1 -0
- package/dist/lib/{file-utils.test.js → core/file-utils.test.js} +240 -214
- package/dist/lib/core/file-utils.test.js.map +1 -0
- package/dist/lib/{input-validator.d.ts → core/input-validator.d.ts} +30 -20
- package/dist/lib/core/input-validator.d.ts.map +1 -0
- package/dist/lib/core/input-validator.fuzz.test.d.ts.map +1 -0
- package/dist/lib/{input-validator.fuzz.test.js → core/input-validator.fuzz.test.js} +41 -29
- package/dist/lib/core/input-validator.fuzz.test.js.map +1 -0
- package/dist/lib/{input-validator.js → core/input-validator.js} +83 -39
- package/dist/lib/core/input-validator.js.map +1 -0
- package/dist/lib/core/input-validator.test.d.ts.map +1 -0
- package/dist/lib/{input-validator.test.js → core/input-validator.test.js} +95 -45
- package/dist/lib/core/input-validator.test.js.map +1 -0
- package/dist/lib/{logger.d.ts → core/logger.d.ts} +4 -18
- package/dist/lib/core/logger.d.ts.map +1 -0
- package/dist/lib/core/logger.js +104 -0
- package/dist/lib/core/logger.js.map +1 -0
- package/dist/lib/core/logger.test.d.ts.map +1 -0
- package/dist/lib/core/logger.test.js.map +1 -0
- package/dist/lib/core/schema-types.d.ts +37 -0
- package/dist/lib/core/schema-types.d.ts.map +1 -0
- package/dist/lib/core/schema-types.js +29 -0
- package/dist/lib/core/schema-types.js.map +1 -0
- package/dist/lib/{server-utils.d.ts → core/server-utils.d.ts} +11 -1
- package/dist/lib/core/server-utils.d.ts.map +1 -0
- package/dist/lib/{server-utils.js → core/server-utils.js} +25 -5
- package/dist/lib/core/server-utils.js.map +1 -0
- package/dist/lib/core/shared-schemas.d.ts +301 -0
- package/dist/lib/core/shared-schemas.d.ts.map +1 -0
- package/dist/lib/core/shared-schemas.js +222 -0
- package/dist/lib/core/shared-schemas.js.map +1 -0
- package/dist/lib/core/shared-schemas.test.d.ts.map +1 -0
- package/dist/lib/{shared-schemas.test.js → core/shared-schemas.test.js} +48 -18
- package/dist/lib/core/shared-schemas.test.js.map +1 -0
- package/dist/lib/core/units.d.ts +67 -0
- package/dist/lib/core/units.d.ts.map +1 -0
- package/dist/lib/core/units.js +88 -0
- package/dist/lib/core/units.js.map +1 -0
- package/dist/lib/cost/cost-estimation.d.ts +264 -0
- package/dist/lib/cost/cost-estimation.d.ts.map +1 -0
- package/dist/lib/cost/cost-estimation.js +541 -0
- package/dist/lib/cost/cost-estimation.js.map +1 -0
- package/dist/lib/cost/cost-estimation.test.d.ts +5 -0
- package/dist/lib/cost/cost-estimation.test.d.ts.map +1 -0
- package/dist/lib/cost/cost-estimation.test.js +701 -0
- package/dist/lib/cost/cost-estimation.test.js.map +1 -0
- package/dist/lib/cost/pricing-cache.d.ts +59 -0
- package/dist/lib/cost/pricing-cache.d.ts.map +1 -0
- package/dist/lib/cost/pricing-cache.js +120 -0
- package/dist/lib/cost/pricing-cache.js.map +1 -0
- package/dist/lib/cost/pricing-cache.test.d.ts +5 -0
- package/dist/lib/cost/pricing-cache.test.d.ts.map +1 -0
- package/dist/lib/cost/pricing-cache.test.js +176 -0
- package/dist/lib/cost/pricing-cache.test.js.map +1 -0
- package/dist/lib/dashboard-file-utils.d.ts +35 -0
- package/dist/lib/dashboard-file-utils.d.ts.map +1 -0
- package/dist/lib/dashboard-file-utils.js +94 -0
- package/dist/lib/dashboard-file-utils.js.map +1 -0
- package/dist/lib/{error-sanitizer.d.ts → errors/error-sanitizer.d.ts} +5 -0
- package/dist/lib/errors/error-sanitizer.d.ts.map +1 -0
- package/dist/lib/{error-sanitizer.js → errors/error-sanitizer.js} +8 -6
- package/dist/lib/errors/error-sanitizer.js.map +1 -0
- package/dist/lib/errors/error-sanitizer.test.d.ts.map +1 -0
- package/dist/lib/{error-sanitizer.test.js → errors/error-sanitizer.test.js} +17 -11
- package/dist/lib/errors/error-sanitizer.test.js.map +1 -0
- package/dist/lib/{error-types.d.ts → errors/error-types.d.ts} +5 -0
- package/dist/lib/errors/error-types.d.ts.map +1 -0
- package/dist/lib/{error-types.js → errors/error-types.js} +34 -1
- package/dist/lib/errors/error-types.js.map +1 -0
- package/dist/lib/errors/error-types.test.d.ts.map +1 -0
- package/dist/lib/{error-types.test.js → errors/error-types.test.js} +51 -1
- package/dist/lib/errors/error-types.test.js.map +1 -0
- package/dist/lib/errors/query-sanitizer.d.ts.map +1 -0
- package/dist/lib/{query-sanitizer.js → errors/query-sanitizer.js} +9 -1
- package/dist/lib/errors/query-sanitizer.js.map +1 -0
- package/dist/lib/errors/query-sanitizer.test.d.ts.map +1 -0
- package/dist/lib/{query-sanitizer.test.js → errors/query-sanitizer.test.js} +9 -6
- package/dist/lib/errors/query-sanitizer.test.js.map +1 -0
- package/dist/lib/exports/confident-export.d.ts +105 -0
- package/dist/lib/exports/confident-export.d.ts.map +1 -0
- package/dist/lib/exports/confident-export.js +385 -0
- package/dist/lib/exports/confident-export.js.map +1 -0
- package/dist/lib/exports/confident-export.test.d.ts +7 -0
- package/dist/lib/exports/confident-export.test.d.ts.map +1 -0
- package/dist/lib/exports/confident-export.test.js +848 -0
- package/dist/lib/exports/confident-export.test.js.map +1 -0
- package/dist/lib/exports/datadog-export.d.ts +200 -0
- package/dist/lib/exports/datadog-export.d.ts.map +1 -0
- package/dist/lib/exports/datadog-export.js +488 -0
- package/dist/lib/exports/datadog-export.js.map +1 -0
- package/dist/lib/exports/datadog-export.test.d.ts +2 -0
- package/dist/lib/exports/datadog-export.test.d.ts.map +1 -0
- package/dist/lib/exports/datadog-export.test.js +890 -0
- package/dist/lib/exports/datadog-export.test.js.map +1 -0
- package/dist/lib/exports/export-config-schemas.d.ts +67 -0
- package/dist/lib/exports/export-config-schemas.d.ts.map +1 -0
- package/dist/lib/exports/export-config-schemas.js +120 -0
- package/dist/lib/exports/export-config-schemas.js.map +1 -0
- package/dist/lib/exports/export-config-schemas.test.d.ts +8 -0
- package/dist/lib/exports/export-config-schemas.test.d.ts.map +1 -0
- package/dist/lib/exports/export-config-schemas.test.js +503 -0
- package/dist/lib/exports/export-config-schemas.test.js.map +1 -0
- package/dist/lib/exports/export-utils.d.ts +127 -0
- package/dist/lib/exports/export-utils.d.ts.map +1 -0
- package/dist/lib/exports/export-utils.js +303 -0
- package/dist/lib/exports/export-utils.js.map +1 -0
- package/dist/lib/exports/export-utils.test.d.ts +5 -0
- package/dist/lib/exports/export-utils.test.d.ts.map +1 -0
- package/dist/lib/exports/export-utils.test.js +344 -0
- package/dist/lib/exports/export-utils.test.js.map +1 -0
- package/dist/lib/exports/langfuse-export.d.ts +129 -0
- package/dist/lib/exports/langfuse-export.d.ts.map +1 -0
- package/dist/lib/exports/langfuse-export.js +370 -0
- package/dist/lib/exports/langfuse-export.js.map +1 -0
- package/dist/lib/exports/langfuse-export.test.d.ts +7 -0
- package/dist/lib/exports/langfuse-export.test.d.ts.map +1 -0
- package/dist/lib/exports/langfuse-export.test.js +1020 -0
- package/dist/lib/exports/langfuse-export.test.js.map +1 -0
- package/dist/lib/{otlp-export.d.ts → exports/otlp-export.d.ts} +3 -2
- package/dist/lib/exports/otlp-export.d.ts.map +1 -0
- package/dist/lib/{otlp-export.js → exports/otlp-export.js} +51 -36
- package/dist/lib/exports/otlp-export.js.map +1 -0
- package/dist/lib/exports/otlp-format-converter.d.ts +70 -0
- package/dist/lib/exports/otlp-format-converter.d.ts.map +1 -0
- package/dist/lib/exports/otlp-format-converter.js +401 -0
- package/dist/lib/exports/otlp-format-converter.js.map +1 -0
- package/dist/lib/exports/otlp-proto-encode.d.ts +53 -0
- package/dist/lib/exports/otlp-proto-encode.d.ts.map +1 -0
- package/dist/lib/exports/otlp-proto-encode.js +165 -0
- package/dist/lib/exports/otlp-proto-encode.js.map +1 -0
- package/dist/lib/exports/otlp-proto-encode.test.d.ts +7 -0
- package/dist/lib/exports/otlp-proto-encode.test.d.ts.map +1 -0
- package/dist/lib/exports/otlp-proto-encode.test.js +997 -0
- package/dist/lib/exports/otlp-proto-encode.test.js.map +1 -0
- package/dist/lib/exports/phoenix-export.d.ts +119 -0
- package/dist/lib/exports/phoenix-export.d.ts.map +1 -0
- package/dist/lib/exports/phoenix-export.js +448 -0
- package/dist/lib/exports/phoenix-export.js.map +1 -0
- package/dist/lib/exports/phoenix-export.test.d.ts +11 -0
- package/dist/lib/exports/phoenix-export.test.d.ts.map +1 -0
- package/dist/lib/exports/phoenix-export.test.js +816 -0
- package/dist/lib/exports/phoenix-export.test.js.map +1 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +31 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/judge/evaluation-hooks-schemas.d.ts +186 -0
- package/dist/lib/judge/evaluation-hooks-schemas.d.ts.map +1 -0
- package/dist/lib/judge/evaluation-hooks-schemas.js +125 -0
- package/dist/lib/judge/evaluation-hooks-schemas.js.map +1 -0
- package/dist/lib/judge/evaluation-hooks.d.ts +88 -0
- package/dist/lib/judge/evaluation-hooks.d.ts.map +1 -0
- package/dist/lib/judge/evaluation-hooks.js +658 -0
- package/dist/lib/judge/evaluation-hooks.js.map +1 -0
- package/dist/lib/judge/evaluation-hooks.test.d.ts +8 -0
- package/dist/lib/judge/evaluation-hooks.test.d.ts.map +1 -0
- package/dist/lib/judge/evaluation-hooks.test.js +934 -0
- package/dist/lib/judge/evaluation-hooks.test.js.map +1 -0
- package/dist/lib/judge/llm-as-judge.d.ts +138 -0
- package/dist/lib/judge/llm-as-judge.d.ts.map +1 -0
- package/dist/lib/judge/llm-as-judge.js +103 -0
- package/dist/lib/judge/llm-as-judge.js.map +1 -0
- package/dist/lib/judge/llm-as-judge.test.d.ts +2 -0
- package/dist/lib/judge/llm-as-judge.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-as-judge.test.js +2179 -0
- package/dist/lib/judge/llm-as-judge.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-bias.d.ts +44 -0
- package/dist/lib/judge/llm-judge-bias.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-bias.js +130 -0
- package/dist/lib/judge/llm-judge-bias.js.map +1 -0
- package/dist/lib/judge/llm-judge-bias.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-bias.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-bias.test.js +380 -0
- package/dist/lib/judge/llm-judge-bias.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-code.d.ts +99 -0
- package/dist/lib/judge/llm-judge-code.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-code.js +261 -0
- package/dist/lib/judge/llm-judge-code.js.map +1 -0
- package/dist/lib/judge/llm-judge-code.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-code.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-code.test.js +981 -0
- package/dist/lib/judge/llm-judge-code.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-config.d.ts +241 -0
- package/dist/lib/judge/llm-judge-config.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-config.js +390 -0
- package/dist/lib/judge/llm-judge-config.js.map +1 -0
- package/dist/lib/judge/llm-judge-config.test.d.ts +5 -0
- package/dist/lib/judge/llm-judge-config.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-config.test.js +392 -0
- package/dist/lib/judge/llm-judge-config.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-constants.d.ts +111 -0
- package/dist/lib/judge/llm-judge-constants.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-constants.js +150 -0
- package/dist/lib/judge/llm-judge-constants.js.map +1 -0
- package/dist/lib/judge/llm-judge-dag.d.ts +57 -0
- package/dist/lib/judge/llm-judge-dag.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-dag.js +217 -0
- package/dist/lib/judge/llm-judge-dag.js.map +1 -0
- package/dist/lib/judge/llm-judge-dag.test.d.ts +8 -0
- package/dist/lib/judge/llm-judge-dag.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-dag.test.js +973 -0
- package/dist/lib/judge/llm-judge-dag.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-domain.d.ts +42 -0
- package/dist/lib/judge/llm-judge-domain.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-domain.js +167 -0
- package/dist/lib/judge/llm-judge-domain.js.map +1 -0
- package/dist/lib/judge/llm-judge-domain.test.d.ts +6 -0
- package/dist/lib/judge/llm-judge-domain.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-domain.test.js +337 -0
- package/dist/lib/judge/llm-judge-domain.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-geval.d.ts +42 -0
- package/dist/lib/judge/llm-judge-geval.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-geval.js +213 -0
- package/dist/lib/judge/llm-judge-geval.js.map +1 -0
- package/dist/lib/judge/llm-judge-geval.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-geval.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-geval.test.js +556 -0
- package/dist/lib/judge/llm-judge-geval.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-otel.test.d.ts +9 -0
- package/dist/lib/judge/llm-judge-otel.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-otel.test.js +91 -0
- package/dist/lib/judge/llm-judge-otel.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-qag.d.ts +38 -0
- package/dist/lib/judge/llm-judge-qag.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-qag.js +205 -0
- package/dist/lib/judge/llm-judge-qag.js.map +1 -0
- package/dist/lib/judge/llm-judge-qag.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-qag.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-qag.test.js +386 -0
- package/dist/lib/judge/llm-judge-qag.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-resilience.d.ts +74 -0
- package/dist/lib/judge/llm-judge-resilience.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-resilience.js +146 -0
- package/dist/lib/judge/llm-judge-resilience.js.map +1 -0
- package/dist/lib/judge/llm-judge-resilience.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-resilience.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-resilience.test.js +353 -0
- package/dist/lib/judge/llm-judge-resilience.test.js.map +1 -0
- package/dist/lib/judge/llm-judge-security.d.ts +106 -0
- package/dist/lib/judge/llm-judge-security.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-security.js +314 -0
- package/dist/lib/judge/llm-judge-security.js.map +1 -0
- package/dist/lib/judge/llm-judge-security.test.d.ts +2 -0
- package/dist/lib/judge/llm-judge-security.test.d.ts.map +1 -0
- package/dist/lib/judge/llm-judge-security.test.js +1011 -0
- package/dist/lib/judge/llm-judge-security.test.js.map +1 -0
- package/dist/lib/observability/context-accumulator.d.ts +32 -0
- package/dist/lib/observability/context-accumulator.d.ts.map +1 -0
- package/dist/lib/observability/context-accumulator.js +87 -0
- package/dist/lib/observability/context-accumulator.js.map +1 -0
- package/dist/lib/observability/evaluation-events.d.ts +35 -0
- package/dist/lib/observability/evaluation-events.d.ts.map +1 -0
- package/dist/lib/observability/evaluation-events.js +90 -0
- package/dist/lib/observability/evaluation-events.js.map +1 -0
- package/dist/lib/observability/file-span-exporter.d.ts +17 -0
- package/dist/lib/observability/file-span-exporter.d.ts.map +1 -0
- package/dist/lib/observability/file-span-exporter.js +49 -0
- package/dist/lib/observability/file-span-exporter.js.map +1 -0
- package/dist/lib/observability/histogram-bucket-constants.d.ts +25 -0
- package/dist/lib/observability/histogram-bucket-constants.d.ts.map +1 -0
- package/dist/lib/observability/histogram-bucket-constants.js +60 -0
- package/dist/lib/observability/histogram-bucket-constants.js.map +1 -0
- package/dist/lib/observability/histogram.d.ts +112 -0
- package/dist/lib/observability/histogram.d.ts.map +1 -0
- package/dist/lib/observability/histogram.js +170 -0
- package/dist/lib/observability/histogram.js.map +1 -0
- package/dist/lib/observability/histogram.test.d.ts +5 -0
- package/dist/lib/observability/histogram.test.d.ts.map +1 -0
- package/dist/lib/observability/histogram.test.js +385 -0
- package/dist/lib/observability/histogram.test.js.map +1 -0
- package/dist/lib/observability/indexer.d.ts +114 -0
- package/dist/lib/observability/indexer.d.ts.map +1 -0
- package/dist/lib/{indexer.js → observability/indexer.js} +65 -16
- package/dist/lib/observability/indexer.js.map +1 -0
- package/dist/lib/observability/indexer.test.d.ts.map +1 -0
- package/dist/lib/{indexer.test.js → observability/indexer.test.js} +94 -77
- package/dist/lib/observability/indexer.test.js.map +1 -0
- package/dist/lib/observability/instrumentation-eval.test.d.ts +5 -0
- package/dist/lib/observability/instrumentation-eval.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation-eval.test.js +63 -0
- package/dist/lib/observability/instrumentation-eval.test.js.map +1 -0
- package/dist/lib/observability/instrumentation-init-errors.test.d.ts +13 -0
- package/dist/lib/observability/instrumentation-init-errors.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation-init-errors.test.js +194 -0
- package/dist/lib/observability/instrumentation-init-errors.test.js.map +1 -0
- package/dist/lib/observability/instrumentation-retry-timeout.test.d.ts +15 -0
- package/dist/lib/observability/instrumentation-retry-timeout.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation-retry-timeout.test.js +188 -0
- package/dist/lib/observability/instrumentation-retry-timeout.test.js.map +1 -0
- package/dist/lib/observability/instrumentation-set-otel.test.d.ts +5 -0
- package/dist/lib/observability/instrumentation-set-otel.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation-set-otel.test.js +59 -0
- package/dist/lib/observability/instrumentation-set-otel.test.js.map +1 -0
- package/dist/lib/observability/instrumentation.d.ts +158 -0
- package/dist/lib/observability/instrumentation.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation.integration.test.d.ts +2 -0
- package/dist/lib/observability/instrumentation.integration.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation.integration.test.js +590 -0
- package/dist/lib/observability/instrumentation.integration.test.js.map +1 -0
- package/dist/lib/observability/instrumentation.js +512 -0
- package/dist/lib/observability/instrumentation.js.map +1 -0
- package/dist/lib/observability/instrumentation.test.d.ts +2 -0
- package/dist/lib/observability/instrumentation.test.d.ts.map +1 -0
- package/dist/lib/observability/instrumentation.test.js +822 -0
- package/dist/lib/observability/instrumentation.test.js.map +1 -0
- package/dist/lib/observability/mcp-semconv-constants.d.ts +98 -0
- package/dist/lib/observability/mcp-semconv-constants.d.ts.map +1 -0
- package/dist/lib/observability/mcp-semconv-constants.js +102 -0
- package/dist/lib/observability/mcp-semconv-constants.js.map +1 -0
- package/dist/lib/observability/mcp-semconv.d.ts +37 -0
- package/dist/lib/observability/mcp-semconv.d.ts.map +1 -0
- package/dist/lib/observability/mcp-semconv.js +87 -0
- package/dist/lib/observability/mcp-semconv.js.map +1 -0
- package/dist/lib/observability/mcp-semconv.test.d.ts +2 -0
- package/dist/lib/observability/mcp-semconv.test.d.ts.map +1 -0
- package/dist/lib/observability/mcp-semconv.test.js +168 -0
- package/dist/lib/observability/mcp-semconv.test.js.map +1 -0
- package/dist/lib/observability/metrics.d.ts +100 -0
- package/dist/lib/observability/metrics.d.ts.map +1 -0
- package/dist/lib/observability/metrics.js +429 -0
- package/dist/lib/observability/metrics.js.map +1 -0
- package/dist/lib/observability/metrics.test.d.ts +5 -0
- package/dist/lib/observability/metrics.test.d.ts.map +1 -0
- package/dist/lib/observability/metrics.test.js +191 -0
- package/dist/lib/observability/metrics.test.js.map +1 -0
- package/dist/lib/observability/observability-test-constants.d.ts +34 -0
- package/dist/lib/observability/observability-test-constants.d.ts.map +1 -0
- package/dist/lib/observability/observability-test-constants.js +55 -0
- package/dist/lib/observability/observability-test-constants.js.map +1 -0
- package/dist/lib/observability/opentelemetry-resources.test.d.ts +2 -0
- package/dist/lib/observability/opentelemetry-resources.test.d.ts.map +1 -0
- package/dist/lib/observability/opentelemetry-resources.test.js +19 -0
- package/dist/lib/observability/opentelemetry-resources.test.js.map +1 -0
- package/dist/lib/observability/parse-stats.d.ts +119 -0
- package/dist/lib/observability/parse-stats.d.ts.map +1 -0
- package/dist/lib/observability/parse-stats.js +207 -0
- package/dist/lib/observability/parse-stats.js.map +1 -0
- package/dist/lib/observability/parse-stats.test.d.ts +5 -0
- package/dist/lib/observability/parse-stats.test.d.ts.map +1 -0
- package/dist/lib/observability/parse-stats.test.js +287 -0
- package/dist/lib/observability/parse-stats.test.js.map +1 -0
- package/dist/lib/observability/render-trace-tree.d.ts +31 -0
- package/dist/lib/observability/render-trace-tree.d.ts.map +1 -0
- package/dist/lib/observability/render-trace-tree.js +95 -0
- package/dist/lib/observability/render-trace-tree.js.map +1 -0
- package/dist/lib/observability/render-trace-tree.test.d.ts +5 -0
- package/dist/lib/observability/render-trace-tree.test.d.ts.map +1 -0
- package/dist/lib/observability/render-trace-tree.test.js +97 -0
- package/dist/lib/observability/render-trace-tree.test.js.map +1 -0
- package/dist/lib/observability/span-attributes.d.ts +27 -0
- package/dist/lib/observability/span-attributes.d.ts.map +1 -0
- package/dist/lib/observability/span-attributes.js +85 -0
- package/dist/lib/observability/span-attributes.js.map +1 -0
- package/dist/lib/observability/trace-anomaly-detector.d.ts +23 -0
- package/dist/lib/observability/trace-anomaly-detector.d.ts.map +1 -0
- package/dist/lib/observability/trace-anomaly-detector.js +211 -0
- package/dist/lib/observability/trace-anomaly-detector.js.map +1 -0
- package/dist/lib/observability/trace-anomaly-detector.test.d.ts +5 -0
- package/dist/lib/observability/trace-anomaly-detector.test.d.ts.map +1 -0
- package/dist/lib/observability/trace-anomaly-detector.test.js +224 -0
- package/dist/lib/observability/trace-anomaly-detector.test.js.map +1 -0
- package/dist/lib/observability/trace-anomaly-schemas.d.ts +189 -0
- package/dist/lib/observability/trace-anomaly-schemas.d.ts.map +1 -0
- package/dist/lib/observability/trace-anomaly-schemas.js +167 -0
- package/dist/lib/observability/trace-anomaly-schemas.js.map +1 -0
- package/dist/lib/privacy/content-redaction.d.ts +141 -0
- package/dist/lib/privacy/content-redaction.d.ts.map +1 -0
- package/dist/lib/privacy/content-redaction.js +210 -0
- package/dist/lib/privacy/content-redaction.js.map +1 -0
- package/dist/lib/privacy/content-redaction.test.d.ts +2 -0
- package/dist/lib/privacy/content-redaction.test.d.ts.map +1 -0
- package/dist/lib/privacy/content-redaction.test.js +302 -0
- package/dist/lib/privacy/content-redaction.test.js.map +1 -0
- package/dist/lib/quality/bucket-utils.d.ts +17 -0
- package/dist/lib/quality/bucket-utils.d.ts.map +1 -0
- package/dist/lib/quality/bucket-utils.js +31 -0
- package/dist/lib/quality/bucket-utils.js.map +1 -0
- package/dist/lib/quality/bucket-utils.test.d.ts +2 -0
- package/dist/lib/quality/bucket-utils.test.d.ts.map +1 -0
- package/dist/lib/quality/bucket-utils.test.js +42 -0
- package/dist/lib/quality/bucket-utils.test.js.map +1 -0
- package/dist/lib/quality/qfe-backtest-detail.test.d.ts +5 -0
- package/dist/lib/quality/qfe-backtest-detail.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-backtest-detail.test.js +179 -0
- package/dist/lib/quality/qfe-backtest-detail.test.js.map +1 -0
- package/dist/lib/quality/qfe-calibration-paths.test.d.ts +5 -0
- package/dist/lib/quality/qfe-calibration-paths.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-calibration-paths.test.js +203 -0
- package/dist/lib/quality/qfe-calibration-paths.test.js.map +1 -0
- package/dist/lib/quality/qfe-correlation-helpers.test.d.ts +6 -0
- package/dist/lib/quality/qfe-correlation-helpers.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-correlation-helpers.test.js +143 -0
- package/dist/lib/quality/qfe-correlation-helpers.test.js.map +1 -0
- package/dist/lib/quality/qfe-cqi-paths.test.d.ts +6 -0
- package/dist/lib/quality/qfe-cqi-paths.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-cqi-paths.test.js +231 -0
- package/dist/lib/quality/qfe-cqi-paths.test.js.map +1 -0
- package/dist/lib/quality/qfe-critic-internals.test.d.ts +6 -0
- package/dist/lib/quality/qfe-critic-internals.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-critic-internals.test.js +191 -0
- package/dist/lib/quality/qfe-critic-internals.test.js.map +1 -0
- package/dist/lib/quality/qfe-derived-paths.test.d.ts +2 -0
- package/dist/lib/quality/qfe-derived-paths.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-derived-paths.test.js +372 -0
- package/dist/lib/quality/qfe-derived-paths.test.js.map +1 -0
- package/dist/lib/quality/qfe-dynamics-paths.test.d.ts +8 -0
- package/dist/lib/quality/qfe-dynamics-paths.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-dynamics-paths.test.js +223 -0
- package/dist/lib/quality/qfe-dynamics-paths.test.js.map +1 -0
- package/dist/lib/quality/qfe-granger-internals.test.d.ts +6 -0
- package/dist/lib/quality/qfe-granger-internals.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-granger-internals.test.js +158 -0
- package/dist/lib/quality/qfe-granger-internals.test.js.map +1 -0
- package/dist/lib/quality/qfe-label-normalize.test.d.ts +7 -0
- package/dist/lib/quality/qfe-label-normalize.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-label-normalize.test.js +332 -0
- package/dist/lib/quality/qfe-label-normalize.test.js.map +1 -0
- package/dist/lib/quality/qfe-ordinal-edge.test.d.ts +6 -0
- package/dist/lib/quality/qfe-ordinal-edge.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-ordinal-edge.test.js +98 -0
- package/dist/lib/quality/qfe-ordinal-edge.test.js.map +1 -0
- package/dist/lib/quality/qfe-roles-detail.test.d.ts +5 -0
- package/dist/lib/quality/qfe-roles-detail.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-roles-detail.test.js +115 -0
- package/dist/lib/quality/qfe-roles-detail.test.js.map +1 -0
- package/dist/lib/quality/qfe-rolling-detail.test.d.ts +7 -0
- package/dist/lib/quality/qfe-rolling-detail.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-rolling-detail.test.js +249 -0
- package/dist/lib/quality/qfe-rolling-detail.test.js.map +1 -0
- package/dist/lib/quality/qfe-stats-internals.test.d.ts +7 -0
- package/dist/lib/quality/qfe-stats-internals.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-stats-internals.test.js +143 -0
- package/dist/lib/quality/qfe-stats-internals.test.js.map +1 -0
- package/dist/lib/quality/qfe-streaming.test.d.ts +5 -0
- package/dist/lib/quality/qfe-streaming.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-streaming.test.js +239 -0
- package/dist/lib/quality/qfe-streaming.test.js.map +1 -0
- package/dist/lib/quality/qfe-sweep-detail.test.d.ts +6 -0
- package/dist/lib/quality/qfe-sweep-detail.test.d.ts.map +1 -0
- package/dist/lib/quality/qfe-sweep-detail.test.js +291 -0
- package/dist/lib/quality/qfe-sweep-detail.test.js.map +1 -0
- package/dist/lib/quality/quality-alerts.d.ts +23 -0
- package/dist/lib/quality/quality-alerts.d.ts.map +1 -0
- package/dist/lib/quality/quality-alerts.js +89 -0
- package/dist/lib/quality/quality-alerts.js.map +1 -0
- package/dist/lib/quality/quality-alerts.test.d.ts +2 -0
- package/dist/lib/quality/quality-alerts.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-alerts.test.js +86 -0
- package/dist/lib/quality/quality-alerts.test.js.map +1 -0
- package/dist/lib/quality/quality-constants.d.ts +294 -0
- package/dist/lib/quality/quality-constants.d.ts.map +1 -0
- package/dist/lib/quality/quality-constants.js +335 -0
- package/dist/lib/quality/quality-constants.js.map +1 -0
- package/dist/lib/quality/quality-feature-engineering.d.ts +1071 -0
- package/dist/lib/quality/quality-feature-engineering.d.ts.map +1 -0
- package/dist/lib/quality/quality-feature-engineering.js +2076 -0
- package/dist/lib/quality/quality-feature-engineering.js.map +1 -0
- package/dist/lib/quality/quality-feature-engineering.test.d.ts +5 -0
- package/dist/lib/quality/quality-feature-engineering.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-feature-engineering.test.js +2908 -0
- package/dist/lib/quality/quality-feature-engineering.test.js.map +1 -0
- package/dist/lib/quality/quality-metrics.d.ts +943 -0
- package/dist/lib/quality/quality-metrics.d.ts.map +1 -0
- package/dist/lib/quality/quality-metrics.js +1151 -0
- package/dist/lib/quality/quality-metrics.js.map +1 -0
- package/dist/lib/quality/quality-metrics.test.d.ts +5 -0
- package/dist/lib/quality/quality-metrics.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-metrics.test.js +2766 -0
- package/dist/lib/quality/quality-metrics.test.js.map +1 -0
- package/dist/lib/quality/quality-multi-agent.d.ts +106 -0
- package/dist/lib/quality/quality-multi-agent.d.ts.map +1 -0
- package/dist/lib/quality/quality-multi-agent.js +124 -0
- package/dist/lib/quality/quality-multi-agent.js.map +1 -0
- package/dist/lib/quality/quality-multi-agent.test.d.ts +6 -0
- package/dist/lib/quality/quality-multi-agent.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-multi-agent.test.js +163 -0
- package/dist/lib/quality/quality-multi-agent.test.js.map +1 -0
- package/dist/lib/quality/quality-sla.d.ts +35 -0
- package/dist/lib/quality/quality-sla.d.ts.map +1 -0
- package/dist/lib/quality/quality-sla.js +62 -0
- package/dist/lib/quality/quality-sla.js.map +1 -0
- package/dist/lib/quality/quality-sla.test.d.ts +5 -0
- package/dist/lib/quality/quality-sla.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-sla.test.js +144 -0
- package/dist/lib/quality/quality-sla.test.js.map +1 -0
- package/dist/lib/quality/quality-test-constants.d.ts +23 -0
- package/dist/lib/quality/quality-test-constants.d.ts.map +1 -0
- package/dist/lib/quality/quality-test-constants.js +25 -0
- package/dist/lib/quality/quality-test-constants.js.map +1 -0
- package/dist/lib/quality/quality-trends.d.ts +101 -0
- package/dist/lib/quality/quality-trends.d.ts.map +1 -0
- package/dist/lib/quality/quality-trends.js +299 -0
- package/dist/lib/quality/quality-trends.js.map +1 -0
- package/dist/lib/quality/quality-trends.test.d.ts +6 -0
- package/dist/lib/quality/quality-trends.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-trends.test.js +377 -0
- package/dist/lib/quality/quality-trends.test.js.map +1 -0
- package/dist/lib/quality/quality-views.d.ts +966 -0
- package/dist/lib/quality/quality-views.d.ts.map +1 -0
- package/dist/lib/quality/quality-views.js +367 -0
- package/dist/lib/quality/quality-views.js.map +1 -0
- package/dist/lib/quality/quality-views.test.d.ts +6 -0
- package/dist/lib/quality/quality-views.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-views.test.js +262 -0
- package/dist/lib/quality/quality-views.test.js.map +1 -0
- package/dist/lib/quality/quality-visualization.d.ts +112 -0
- package/dist/lib/quality/quality-visualization.d.ts.map +1 -0
- package/dist/lib/quality/quality-visualization.js +136 -0
- package/dist/lib/quality/quality-visualization.js.map +1 -0
- package/dist/lib/quality/quality-visualization.test.d.ts +5 -0
- package/dist/lib/quality/quality-visualization.test.d.ts.map +1 -0
- package/dist/lib/quality/quality-visualization.test.js +189 -0
- package/dist/lib/quality/quality-visualization.test.js.map +1 -0
- package/dist/lib/resilience/cache.d.ts +56 -0
- package/dist/lib/resilience/cache.d.ts.map +1 -0
- package/dist/lib/resilience/cache.js +96 -0
- package/dist/lib/resilience/cache.js.map +1 -0
- package/dist/lib/resilience/cache.test.d.ts.map +1 -0
- package/dist/lib/{cache.test.js → resilience/cache.test.js} +21 -20
- package/dist/lib/resilience/cache.test.js.map +1 -0
- package/dist/lib/resilience/circuit-breaker.d.ts +147 -0
- package/dist/lib/resilience/circuit-breaker.d.ts.map +1 -0
- package/dist/lib/resilience/circuit-breaker.js +251 -0
- package/dist/lib/resilience/circuit-breaker.js.map +1 -0
- package/dist/lib/resilience/circuit-breaker.test.d.ts.map +1 -0
- package/dist/lib/{circuit-breaker.test.js → resilience/circuit-breaker.test.js} +29 -26
- package/dist/lib/resilience/circuit-breaker.test.js.map +1 -0
- package/dist/lib/{toon-encoder.d.ts → resilience/toon-encoder.d.ts} +6 -1
- package/dist/lib/resilience/toon-encoder.d.ts.map +1 -0
- package/dist/lib/{toon-encoder.js → resilience/toon-encoder.js} +7 -2
- package/dist/lib/resilience/toon-encoder.js.map +1 -0
- package/dist/lib/resilience/toon-encoder.test.d.ts.map +1 -0
- package/dist/lib/{toon-encoder.test.js → resilience/toon-encoder.test.js} +7 -6
- package/dist/lib/resilience/toon-encoder.test.js.map +1 -0
- package/dist/lib/testing/mock-llm-builder.d.ts +139 -0
- package/dist/lib/testing/mock-llm-builder.d.ts.map +1 -0
- package/dist/lib/testing/mock-llm-builder.js +254 -0
- package/dist/lib/testing/mock-llm-builder.js.map +1 -0
- package/dist/lib/testing/mock-llm-builder.test.d.ts +5 -0
- package/dist/lib/testing/mock-llm-builder.test.d.ts.map +1 -0
- package/dist/lib/testing/mock-llm-builder.test.js +304 -0
- package/dist/lib/testing/mock-llm-builder.test.js.map +1 -0
- package/dist/lib/validation/api-schemas.d.ts +705 -0
- package/dist/lib/validation/api-schemas.d.ts.map +1 -0
- package/dist/lib/validation/api-schemas.js +351 -0
- package/dist/lib/validation/api-schemas.js.map +1 -0
- package/dist/lib/validation/api-schemas.test.d.ts +5 -0
- package/dist/lib/validation/api-schemas.test.d.ts.map +1 -0
- package/dist/lib/validation/api-schemas.test.js +427 -0
- package/dist/lib/validation/api-schemas.test.js.map +1 -0
- package/dist/lib/validation/dashboard-schemas.d.ts +203 -0
- package/dist/lib/validation/dashboard-schemas.d.ts.map +1 -0
- package/dist/lib/validation/dashboard-schemas.js +186 -0
- package/dist/lib/validation/dashboard-schemas.js.map +1 -0
- package/dist/lib/validation/dashboard-schemas.test.d.ts +5 -0
- package/dist/lib/validation/dashboard-schemas.test.d.ts.map +1 -0
- package/dist/lib/validation/dashboard-schemas.test.js +353 -0
- package/dist/lib/validation/dashboard-schemas.test.js.map +1 -0
- package/dist/server.d.ts +7 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +172 -102
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +102 -95
- package/dist/server.test.js.map +1 -1
- package/dist/test-helpers/assertions.d.ts +6 -0
- package/dist/test-helpers/assertions.d.ts.map +1 -0
- package/dist/test-helpers/assertions.js +11 -0
- package/dist/test-helpers/assertions.js.map +1 -0
- package/dist/test-helpers/env-utils.d.ts +0 -64
- package/dist/test-helpers/env-utils.d.ts.map +1 -1
- package/dist/test-helpers/env-utils.js +0 -100
- package/dist/test-helpers/env-utils.js.map +1 -1
- package/dist/test-helpers/fuzz-generators.d.ts.map +1 -1
- package/dist/test-helpers/fuzz-generators.js +62 -22
- package/dist/test-helpers/fuzz-generators.js.map +1 -1
- package/dist/test-helpers/index.d.ts +3 -2
- package/dist/test-helpers/index.d.ts.map +1 -1
- package/dist/test-helpers/index.js +4 -2
- package/dist/test-helpers/index.js.map +1 -1
- package/dist/test-helpers/memfs-utils.test.js +81 -76
- package/dist/test-helpers/memfs-utils.test.js.map +1 -1
- package/dist/test-helpers/mock-backends.d.ts +19 -17
- package/dist/test-helpers/mock-backends.d.ts.map +1 -1
- package/dist/test-helpers/mock-backends.js +16 -4
- package/dist/test-helpers/mock-backends.js.map +1 -1
- package/dist/test-helpers/mock-backends.test.js +43 -112
- package/dist/test-helpers/mock-backends.test.js.map +1 -1
- package/dist/test-helpers/race-condition-helpers.d.ts.map +1 -1
- package/dist/test-helpers/race-condition-helpers.js +3 -2
- package/dist/test-helpers/race-condition-helpers.js.map +1 -1
- package/dist/test-helpers/schema-validators.d.ts +2 -2
- package/dist/test-helpers/schema-validators.d.ts.map +1 -1
- package/dist/test-helpers/schema-validators.js +35 -31
- package/dist/test-helpers/schema-validators.js.map +1 -1
- package/dist/test-helpers/test-constants.d.ts +74 -0
- package/dist/test-helpers/test-constants.d.ts.map +1 -0
- package/dist/test-helpers/test-constants.js +78 -0
- package/dist/test-helpers/test-constants.js.map +1 -0
- package/dist/test-helpers/test-data-builders.d.ts +25 -7
- package/dist/test-helpers/test-data-builders.d.ts.map +1 -1
- package/dist/test-helpers/test-data-builders.js +32 -9
- package/dist/test-helpers/test-data-builders.js.map +1 -1
- package/dist/test-helpers/test-data-builders.test.js +116 -107
- package/dist/test-helpers/test-data-builders.test.js.map +1 -1
- package/dist/test-helpers/tool-validators.d.ts +1 -1
- package/dist/test-helpers/tool-validators.d.ts.map +1 -1
- package/dist/test-helpers/tool-validators.js +10 -10
- package/dist/test-helpers/tool-validators.js.map +1 -1
- package/dist/tools/audit-trail.d.ts +170 -0
- package/dist/tools/audit-trail.d.ts.map +1 -0
- package/dist/tools/audit-trail.js +109 -0
- package/dist/tools/audit-trail.js.map +1 -0
- package/dist/tools/audit-trail.test.d.ts +5 -0
- package/dist/tools/audit-trail.test.d.ts.map +1 -0
- package/dist/tools/audit-trail.test.js +122 -0
- package/dist/tools/audit-trail.test.js.map +1 -0
- package/dist/tools/context-stats.d.ts +6 -20
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +106 -90
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/context-stats.test.js +109 -60
- package/dist/tools/context-stats.test.js.map +1 -1
- package/dist/tools/detect-trace-anomalies.d.ts +123 -0
- package/dist/tools/detect-trace-anomalies.d.ts.map +1 -0
- package/dist/tools/detect-trace-anomalies.js +66 -0
- package/dist/tools/detect-trace-anomalies.js.map +1 -0
- package/dist/tools/estimate-cost.d.ts +77 -0
- package/dist/tools/estimate-cost.d.ts.map +1 -0
- package/dist/tools/estimate-cost.js +104 -0
- package/dist/tools/estimate-cost.js.map +1 -0
- package/dist/tools/estimate-cost.test.d.ts +5 -0
- package/dist/tools/estimate-cost.test.d.ts.map +1 -0
- package/dist/tools/estimate-cost.test.js +343 -0
- package/dist/tools/estimate-cost.test.js.map +1 -0
- package/dist/tools/export-base.d.ts +77 -0
- package/dist/tools/export-base.d.ts.map +1 -0
- package/dist/tools/export-base.js +150 -0
- package/dist/tools/export-base.js.map +1 -0
- package/dist/tools/export-base.test.d.ts +18 -0
- package/dist/tools/export-base.test.d.ts.map +1 -0
- package/dist/tools/export-base.test.js +220 -0
- package/dist/tools/export-base.test.js.map +1 -0
- package/dist/tools/export-confident.d.ts +149 -0
- package/dist/tools/export-confident.d.ts.map +1 -0
- package/dist/tools/export-confident.js +36 -0
- package/dist/tools/export-confident.js.map +1 -0
- package/dist/tools/export-confident.test.d.ts +7 -0
- package/dist/tools/export-confident.test.d.ts.map +1 -0
- package/dist/tools/export-confident.test.js +336 -0
- package/dist/tools/export-confident.test.js.map +1 -0
- package/dist/tools/export-datadog.d.ts +121 -0
- package/dist/tools/export-datadog.d.ts.map +1 -0
- package/dist/tools/export-datadog.js +158 -0
- package/dist/tools/export-datadog.js.map +1 -0
- package/dist/tools/export-datadog.test.d.ts +8 -0
- package/dist/tools/export-datadog.test.d.ts.map +1 -0
- package/dist/tools/export-datadog.test.js +376 -0
- package/dist/tools/export-datadog.test.js.map +1 -0
- package/dist/tools/export-jaeger.d.ts +100 -0
- package/dist/tools/export-jaeger.d.ts.map +1 -0
- package/dist/tools/export-jaeger.js +154 -0
- package/dist/tools/export-jaeger.js.map +1 -0
- package/dist/tools/export-jaeger.test.d.ts +2 -0
- package/dist/tools/export-jaeger.test.d.ts.map +1 -0
- package/dist/tools/export-jaeger.test.js +113 -0
- package/dist/tools/export-jaeger.test.js.map +1 -0
- package/dist/tools/export-langfuse.d.ts +135 -0
- package/dist/tools/export-langfuse.d.ts.map +1 -0
- package/dist/tools/export-langfuse.js +33 -0
- package/dist/tools/export-langfuse.js.map +1 -0
- package/dist/tools/export-langfuse.test.d.ts +7 -0
- package/dist/tools/export-langfuse.test.d.ts.map +1 -0
- package/dist/tools/export-langfuse.test.js +292 -0
- package/dist/tools/export-langfuse.test.js.map +1 -0
- package/dist/tools/export-phoenix.d.ts +170 -0
- package/dist/tools/export-phoenix.d.ts.map +1 -0
- package/dist/tools/export-phoenix.js +47 -0
- package/dist/tools/export-phoenix.js.map +1 -0
- package/dist/tools/export-phoenix.test.d.ts +7 -0
- package/dist/tools/export-phoenix.test.d.ts.map +1 -0
- package/dist/tools/export-phoenix.test.js +317 -0
- package/dist/tools/export-phoenix.test.js.map +1 -0
- package/dist/tools/get-trace-url.d.ts +2 -10
- package/dist/tools/get-trace-url.d.ts.map +1 -1
- package/dist/tools/get-trace-url.js +5 -8
- package/dist/tools/get-trace-url.js.map +1 -1
- package/dist/tools/get-trace-url.test.js +81 -399
- package/dist/tools/get-trace-url.test.js.map +1 -1
- package/dist/tools/hallucination-detection.d.ts +203 -0
- package/dist/tools/hallucination-detection.d.ts.map +1 -0
- package/dist/tools/hallucination-detection.js +189 -0
- package/dist/tools/hallucination-detection.js.map +1 -0
- package/dist/tools/hallucination-detection.test.d.ts +5 -0
- package/dist/tools/hallucination-detection.test.d.ts.map +1 -0
- package/dist/tools/hallucination-detection.test.js +529 -0
- package/dist/tools/hallucination-detection.test.js.map +1 -0
- package/dist/tools/health-check.d.ts +35 -16
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +101 -85
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/health-check.test.js +72 -165
- package/dist/tools/health-check.test.js.map +1 -1
- package/dist/tools/index.d.ts +19 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +19 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/ingest-constants.d.ts +8 -0
- package/dist/tools/ingest-constants.d.ts.map +1 -0
- package/dist/tools/ingest-constants.js +8 -0
- package/dist/tools/ingest-constants.js.map +1 -0
- package/dist/tools/ingest-spans.d.ts +45 -0
- package/dist/tools/ingest-spans.d.ts.map +1 -0
- package/dist/tools/ingest-spans.js +129 -0
- package/dist/tools/ingest-spans.js.map +1 -0
- package/dist/tools/ingest-spans.test.d.ts +5 -0
- package/dist/tools/ingest-spans.test.d.ts.map +1 -0
- package/dist/tools/ingest-spans.test.js +250 -0
- package/dist/tools/ingest-spans.test.js.map +1 -0
- package/dist/tools/ingest-traces.d.ts +76 -0
- package/dist/tools/ingest-traces.d.ts.map +1 -0
- package/dist/tools/ingest-traces.js +164 -0
- package/dist/tools/ingest-traces.js.map +1 -0
- package/dist/tools/ingest-traces.test.d.ts +5 -0
- package/dist/tools/ingest-traces.test.d.ts.map +1 -0
- package/dist/tools/ingest-traces.test.js +483 -0
- package/dist/tools/ingest-traces.test.js.map +1 -0
- package/dist/tools/inject-evaluations.d.ts +254 -0
- package/dist/tools/inject-evaluations.d.ts.map +1 -0
- package/dist/tools/inject-evaluations.js +133 -0
- package/dist/tools/inject-evaluations.js.map +1 -0
- package/dist/tools/inject-evaluations.test.d.ts +5 -0
- package/dist/tools/inject-evaluations.test.d.ts.map +1 -0
- package/dist/tools/inject-evaluations.test.js +371 -0
- package/dist/tools/inject-evaluations.test.js.map +1 -0
- package/dist/tools/manage-datasets.d.ts +850 -0
- package/dist/tools/manage-datasets.d.ts.map +1 -0
- package/dist/tools/manage-datasets.js +139 -0
- package/dist/tools/manage-datasets.js.map +1 -0
- package/dist/tools/manage-datasets.test.d.ts +5 -0
- package/dist/tools/manage-datasets.test.d.ts.map +1 -0
- package/dist/tools/manage-datasets.test.js +430 -0
- package/dist/tools/manage-datasets.test.js.map +1 -0
- package/dist/tools/multi-agent-coordination.d.ts +178 -0
- package/dist/tools/multi-agent-coordination.d.ts.map +1 -0
- package/dist/tools/multi-agent-coordination.js +270 -0
- package/dist/tools/multi-agent-coordination.js.map +1 -0
- package/dist/tools/multi-agent-coordination.test.d.ts +5 -0
- package/dist/tools/multi-agent-coordination.test.d.ts.map +1 -0
- package/dist/tools/multi-agent-coordination.test.js +530 -0
- package/dist/tools/multi-agent-coordination.test.js.map +1 -0
- package/dist/tools/query-evaluations.d.ts +154 -91
- package/dist/tools/query-evaluations.d.ts.map +1 -1
- package/dist/tools/query-evaluations.js +206 -169
- package/dist/tools/query-evaluations.js.map +1 -1
- package/dist/tools/query-evaluations.test.js +386 -391
- package/dist/tools/query-evaluations.test.js.map +1 -1
- package/dist/tools/query-llm-events.d.ts +100 -75
- package/dist/tools/query-llm-events.d.ts.map +1 -1
- package/dist/tools/query-llm-events.js +106 -80
- package/dist/tools/query-llm-events.js.map +1 -1
- package/dist/tools/query-llm-events.test.js +183 -346
- package/dist/tools/query-llm-events.test.js.map +1 -1
- package/dist/tools/query-logs.d.ts +45 -58
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +54 -101
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.js +118 -314
- package/dist/tools/query-logs.test.js.map +1 -1
- package/dist/tools/query-metric-histograms.d.ts +112 -0
- package/dist/tools/query-metric-histograms.d.ts.map +1 -0
- package/dist/tools/query-metric-histograms.js +69 -0
- package/dist/tools/query-metric-histograms.js.map +1 -0
- package/dist/tools/query-metric-histograms.test.d.ts +5 -0
- package/dist/tools/query-metric-histograms.test.d.ts.map +1 -0
- package/dist/tools/query-metric-histograms.test.js +209 -0
- package/dist/tools/query-metric-histograms.test.js.map +1 -0
- package/dist/tools/query-metrics.d.ts +159 -60
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +133 -111
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.js +314 -389
- package/dist/tools/query-metrics.test.js.map +1 -1
- package/dist/tools/query-regressions.d.ts +76 -0
- package/dist/tools/query-regressions.d.ts.map +1 -0
- package/dist/tools/query-regressions.js +122 -0
- package/dist/tools/query-regressions.js.map +1 -0
- package/dist/tools/query-regressions.test.d.ts +8 -0
- package/dist/tools/query-regressions.test.d.ts.map +1 -0
- package/dist/tools/query-regressions.test.js +129 -0
- package/dist/tools/query-regressions.test.js.map +1 -0
- package/dist/tools/query-traces.d.ts +103 -71
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +75 -106
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.js +140 -846
- package/dist/tools/query-traces.test.js.map +1 -1
- package/dist/tools/query-verifications.d.ts +123 -0
- package/dist/tools/query-verifications.d.ts.map +1 -0
- package/dist/tools/query-verifications.js +102 -0
- package/dist/tools/query-verifications.js.map +1 -0
- package/dist/tools/query-verifications.test.d.ts +5 -0
- package/dist/tools/query-verifications.test.d.ts.map +1 -0
- package/dist/tools/query-verifications.test.js +163 -0
- package/dist/tools/query-verifications.test.js.map +1 -0
- package/dist/tools/routing-telemetry.d.ts +168 -0
- package/dist/tools/routing-telemetry.d.ts.map +1 -0
- package/dist/tools/routing-telemetry.js +267 -0
- package/dist/tools/routing-telemetry.js.map +1 -0
- package/dist/tools/routing-telemetry.test.d.ts +5 -0
- package/dist/tools/routing-telemetry.test.d.ts.map +1 -0
- package/dist/tools/routing-telemetry.test.js +747 -0
- package/dist/tools/routing-telemetry.test.js.map +1 -0
- package/dist/tools/setup-claudeignore.d.ts +4 -32
- package/dist/tools/setup-claudeignore.d.ts.map +1 -1
- package/dist/tools/setup-claudeignore.js +18 -22
- package/dist/tools/setup-claudeignore.js.map +1 -1
- package/dist/tools/setup-claudeignore.test.js +50 -49
- package/dist/tools/setup-claudeignore.test.js.map +1 -1
- package/dist/tools/token-budget.d.ts +170 -0
- package/dist/tools/token-budget.d.ts.map +1 -0
- package/dist/tools/token-budget.js +219 -0
- package/dist/tools/token-budget.js.map +1 -0
- package/dist/tools/token-budget.test.d.ts +5 -0
- package/dist/tools/token-budget.test.d.ts.map +1 -0
- package/dist/tools/token-budget.test.js +293 -0
- package/dist/tools/token-budget.test.js.map +1 -0
- package/package.json +76 -6
- package/dist/backends/local-jsonl.test.d.ts +0 -2
- package/dist/backends/local-jsonl.test.d.ts.map +0 -1
- package/dist/backends/local-jsonl.test.js +0 -4651
- package/dist/backends/local-jsonl.test.js.map +0 -1
- package/dist/backends/signoz-api-circuit-breaker.test.d.ts +0 -6
- package/dist/backends/signoz-api-circuit-breaker.test.d.ts.map +0 -1
- package/dist/backends/signoz-api-circuit-breaker.test.js +0 -548
- package/dist/backends/signoz-api-circuit-breaker.test.js.map +0 -1
- package/dist/backends/signoz-api-rate-limiter.test.d.ts +0 -6
- package/dist/backends/signoz-api-rate-limiter.test.d.ts.map +0 -1
- package/dist/backends/signoz-api-rate-limiter.test.js +0 -389
- package/dist/backends/signoz-api-rate-limiter.test.js.map +0 -1
- package/dist/backends/signoz-api-ssrf.test.d.ts +0 -6
- package/dist/backends/signoz-api-ssrf.test.d.ts.map +0 -1
- package/dist/backends/signoz-api-ssrf.test.js +0 -216
- package/dist/backends/signoz-api-ssrf.test.js.map +0 -1
- package/dist/backends/signoz-api-test-helpers.d.ts +0 -80
- package/dist/backends/signoz-api-test-helpers.d.ts.map +0 -1
- package/dist/backends/signoz-api-test-helpers.js +0 -79
- package/dist/backends/signoz-api-test-helpers.js.map +0 -1
- package/dist/backends/signoz-api.d.ts +0 -95
- package/dist/backends/signoz-api.d.ts.map +0 -1
- package/dist/backends/signoz-api.integration.test.d.ts +0 -8
- package/dist/backends/signoz-api.integration.test.d.ts.map +0 -1
- package/dist/backends/signoz-api.integration.test.js +0 -137
- package/dist/backends/signoz-api.integration.test.js.map +0 -1
- package/dist/backends/signoz-api.js +0 -1016
- package/dist/backends/signoz-api.js.map +0 -1
- package/dist/backends/signoz-api.test.d.ts +0 -11
- package/dist/backends/signoz-api.test.d.ts.map +0 -1
- package/dist/backends/signoz-api.test.js +0 -831
- package/dist/backends/signoz-api.test.js.map +0 -1
- package/dist/lib/cache.d.ts +0 -77
- package/dist/lib/cache.d.ts.map +0 -1
- package/dist/lib/cache.js +0 -119
- package/dist/lib/cache.js.map +0 -1
- package/dist/lib/cache.test.d.ts.map +0 -1
- package/dist/lib/cache.test.js.map +0 -1
- package/dist/lib/circuit-breaker.d.ts +0 -83
- package/dist/lib/circuit-breaker.d.ts.map +0 -1
- package/dist/lib/circuit-breaker.js +0 -125
- package/dist/lib/circuit-breaker.js.map +0 -1
- package/dist/lib/circuit-breaker.test.d.ts.map +0 -1
- package/dist/lib/circuit-breaker.test.js.map +0 -1
- package/dist/lib/constants-symlink.test.d.ts.map +0 -1
- package/dist/lib/constants-symlink.test.js.map +0 -1
- package/dist/lib/constants.d.ts +0 -108
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/constants.js +0 -350
- package/dist/lib/constants.js.map +0 -1
- package/dist/lib/constants.test.d.ts.map +0 -1
- package/dist/lib/constants.test.js.map +0 -1
- package/dist/lib/edge-cases.test.d.ts.map +0 -1
- package/dist/lib/edge-cases.test.js.map +0 -1
- package/dist/lib/error-sanitizer.d.ts.map +0 -1
- package/dist/lib/error-sanitizer.js.map +0 -1
- package/dist/lib/error-sanitizer.test.d.ts.map +0 -1
- package/dist/lib/error-sanitizer.test.js.map +0 -1
- package/dist/lib/error-types.d.ts.map +0 -1
- package/dist/lib/error-types.js.map +0 -1
- package/dist/lib/error-types.test.d.ts.map +0 -1
- package/dist/lib/error-types.test.js.map +0 -1
- package/dist/lib/file-utils.d.ts.map +0 -1
- package/dist/lib/file-utils.js.map +0 -1
- package/dist/lib/file-utils.test.d.ts.map +0 -1
- package/dist/lib/file-utils.test.js.map +0 -1
- package/dist/lib/indexer.d.ts +0 -96
- package/dist/lib/indexer.d.ts.map +0 -1
- package/dist/lib/indexer.js.map +0 -1
- package/dist/lib/indexer.test.d.ts.map +0 -1
- package/dist/lib/indexer.test.js.map +0 -1
- package/dist/lib/input-validator.d.ts.map +0 -1
- package/dist/lib/input-validator.fuzz.test.d.ts.map +0 -1
- package/dist/lib/input-validator.fuzz.test.js.map +0 -1
- package/dist/lib/input-validator.js.map +0 -1
- package/dist/lib/input-validator.test.d.ts.map +0 -1
- package/dist/lib/input-validator.test.js.map +0 -1
- package/dist/lib/logger.d.ts.map +0 -1
- package/dist/lib/logger.js +0 -81
- package/dist/lib/logger.js.map +0 -1
- package/dist/lib/logger.test.d.ts.map +0 -1
- package/dist/lib/logger.test.js.map +0 -1
- package/dist/lib/otlp-export.d.ts.map +0 -1
- package/dist/lib/otlp-export.js.map +0 -1
- package/dist/lib/query-sanitizer.d.ts.map +0 -1
- package/dist/lib/query-sanitizer.js.map +0 -1
- package/dist/lib/query-sanitizer.test.d.ts.map +0 -1
- package/dist/lib/query-sanitizer.test.js.map +0 -1
- package/dist/lib/server-utils.d.ts.map +0 -1
- package/dist/lib/server-utils.js.map +0 -1
- package/dist/lib/shared-schemas.d.ts +0 -81
- package/dist/lib/shared-schemas.d.ts.map +0 -1
- package/dist/lib/shared-schemas.js +0 -80
- package/dist/lib/shared-schemas.js.map +0 -1
- package/dist/lib/shared-schemas.test.d.ts.map +0 -1
- package/dist/lib/shared-schemas.test.js.map +0 -1
- package/dist/lib/toon-encoder.d.ts.map +0 -1
- package/dist/lib/toon-encoder.js.map +0 -1
- package/dist/lib/toon-encoder.test.d.ts.map +0 -1
- package/dist/lib/toon-encoder.test.js.map +0 -1
- package/dist/tools/signoz.integration.test.d.ts +0 -8
- package/dist/tools/signoz.integration.test.d.ts.map +0 -1
- package/dist/tools/signoz.integration.test.js +0 -141
- package/dist/tools/signoz.integration.test.js.map +0 -1
- /package/dist/lib/{constants-symlink.test.d.ts → core/constants-symlink.test.d.ts} +0 -0
- /package/dist/lib/{constants.test.d.ts → core/constants.test.d.ts} +0 -0
- /package/dist/lib/{edge-cases.test.d.ts → core/edge-cases.test.d.ts} +0 -0
- /package/dist/lib/{file-utils.test.d.ts → core/file-utils.test.d.ts} +0 -0
- /package/dist/lib/{input-validator.fuzz.test.d.ts → core/input-validator.fuzz.test.d.ts} +0 -0
- /package/dist/lib/{input-validator.test.d.ts → core/input-validator.test.d.ts} +0 -0
- /package/dist/lib/{logger.test.d.ts → core/logger.test.d.ts} +0 -0
- /package/dist/lib/{logger.test.js → core/logger.test.js} +0 -0
- /package/dist/lib/{shared-schemas.test.d.ts → core/shared-schemas.test.d.ts} +0 -0
- /package/dist/lib/{error-sanitizer.test.d.ts → errors/error-sanitizer.test.d.ts} +0 -0
- /package/dist/lib/{error-types.test.d.ts → errors/error-types.test.d.ts} +0 -0
- /package/dist/lib/{query-sanitizer.d.ts → errors/query-sanitizer.d.ts} +0 -0
- /package/dist/lib/{query-sanitizer.test.d.ts → errors/query-sanitizer.test.d.ts} +0 -0
- /package/dist/lib/{indexer.test.d.ts → observability/indexer.test.d.ts} +0 -0
- /package/dist/lib/{cache.test.d.ts → resilience/cache.test.d.ts} +0 -0
- /package/dist/lib/{circuit-breaker.test.d.ts → resilience/circuit-breaker.test.d.ts} +0 -0
- /package/dist/lib/{toon-encoder.test.d.ts → resilience/toon-encoder.test.d.ts} +0 -0
|
@@ -0,0 +1,2076 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Engineering for Evaluation Analytics (Design Doc Section 16)
|
|
3
|
+
*
|
|
4
|
+
* Derived features, adaptive score scaling, label encoding, and composite metrics
|
|
5
|
+
* computed from existing evaluation data. Enhances anomaly detection, correlation
|
|
6
|
+
* discovery, and role-appropriate information presentation.
|
|
7
|
+
*
|
|
8
|
+
* @see quality-metrics.ts for core types
|
|
9
|
+
* @see frontend/docs/llm-explainability-design.md Section 16
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { normalizedScoreSchema } from '../core/shared-schemas.js';
|
|
15
|
+
import { pipe, map, filter, sort } from 'remeda';
|
|
16
|
+
import { roundTo, SCORE_PRECISION, } from './quality-metrics.js';
|
|
17
|
+
import { AUDITOR_EXPLANATION_TRUNCATION, AUDITOR_MAX_WORST_EVALUATIONS, BINARY_SCALE_EXCELLENT_THRESHOLD, CONFIRMATION_MIN_CONSECUTIVE_BREACHES, COVERAGE_SAMPLE_HIGH_THRESHOLD, COVERAGE_SAMPLE_LOW_THRESHOLD, DYNAMICS_CONFIDENCE_BASE, DYNAMICS_CONFIDENCE_MAX, DYNAMICS_CONFIDENCE_STEP, EWMA_BASELINE_FRACTION, EWMA_BASELINE_MIN_POINTS, EWMA_DRIFT_MIN_VALUES, EWMA_RANGE_FALLBACK_RATIO, EXECUTIVE_EXPLANATION_TRUNCATION, EXECUTIVE_MAX_WORST_EVALUATIONS, GRANGER_FIXED_LAG_ORDER, LATENCY_NORMALIZATION_MAX_SECONDS, LATENCY_SKEW_SIGNAL_THRESHOLD, LOG_SCALE_DIVISOR, MAD_CONSISTENCY_FACTOR, MIN_PERIOD_HOURS, NUMERICAL_MIN_DENOMINATOR, OPERATOR_MAX_WORST_EVALUATIONS, SCORE_BAND_ADEQUATE, SCORE_BAND_EXCELLENT, SCORE_BAND_GOOD, SCORE_BAND_POOR, STEP_SCALE_ADEQUATE_THRESHOLD, STEP_SCALE_EXCELLENT_THRESHOLD, DEGRADATION_BASELINE_RATIO, DEGRADATION_STATE_FILE, MIN_BUCKETS_FOR_SIGNAL, CALIBRATION_WINDOW_DAYS, PSI_RECALIBRATION_THRESHOLD, CALIBRATION_STATE_FILE, DEFAULT_BIN_COUNT, PSI_LOG_EPSILON, AHP_RANDOM_INDEX, AHP_CRITIC_DEFAULT_ALPHA, TUNED_CQI_WEIGHTS_FEATURE_VERSION, } from './quality-constants.js';
|
|
18
|
+
import { TIME_MS, } from '../core/units.js';
|
|
19
|
+
import { TEXT_PREVIEW_LIMIT } from '../core/constants.js';
|
|
20
|
+
import * as ss from 'simple-statistics';
|
|
21
|
+
import tCdf from '@stdlib/stats-base-dists-t-cdf';
|
|
22
|
+
import fCdf from '@stdlib/stats-base-dists-f-cdf';
|
|
23
|
+
import { DDSketch } from '@datadog/sketches-js';
|
|
24
|
+
import { Matrix, solve } from 'ml-matrix';
|
|
25
|
+
const QUANTILE_P10 = 0.1;
|
|
26
|
+
const QUANTILE_P25 = 0.25;
|
|
27
|
+
const QUANTILE_P50 = 0.5;
|
|
28
|
+
const QUANTILE_P75 = 0.75;
|
|
29
|
+
const QUANTILE_P90 = 0.9;
|
|
30
|
+
const DEFAULT_SENSITIVITY_DELTA = 0.05;
|
|
31
|
+
const CORRELATION_MIN_POINTS = 3;
|
|
32
|
+
const FORMAT_JSON_INDENT_SPACES = 2;
|
|
33
|
+
/** Default scale strategy per metric */
|
|
34
|
+
export const METRIC_SCALE_STRATEGY = {
|
|
35
|
+
relevance: 'quantile',
|
|
36
|
+
faithfulness: 'binary',
|
|
37
|
+
coherence: 'uniform',
|
|
38
|
+
hallucination: 'log',
|
|
39
|
+
task_completion: 'step',
|
|
40
|
+
tool_correctness: 'categorical',
|
|
41
|
+
evaluation_latency: 'percentile_rank',
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Map a score value to a 5-band color band with direction awareness.
|
|
45
|
+
* For 'minimize' metrics (hallucination, latency), low scores are excellent.
|
|
46
|
+
* For 'maximize' metrics (relevance, faithfulness), high scores are excellent.
|
|
47
|
+
*/
|
|
48
|
+
export function scoreColorBand(value, direction = 'maximize') {
|
|
49
|
+
const v = direction === 'minimize' ? 1 - value : value;
|
|
50
|
+
if (v >= SCORE_BAND_EXCELLENT)
|
|
51
|
+
return 'excellent';
|
|
52
|
+
if (v >= SCORE_BAND_GOOD)
|
|
53
|
+
return 'good';
|
|
54
|
+
if (v >= SCORE_BAND_ADEQUATE)
|
|
55
|
+
return 'adequate';
|
|
56
|
+
if (v >= SCORE_BAND_POOR)
|
|
57
|
+
return 'poor';
|
|
58
|
+
return 'failing';
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Zod Schemas for Feature Engineering Types
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/**
|
|
64
|
+
* Zod schemas for runtime validation of quality feature types.
|
|
65
|
+
* Used for state persistence and API data validation.
|
|
66
|
+
*/
|
|
67
|
+
export const percentileDistributionSchema = z.object({
|
|
68
|
+
p10: normalizedScoreSchema,
|
|
69
|
+
p25: normalizedScoreSchema,
|
|
70
|
+
p50: normalizedScoreSchema,
|
|
71
|
+
p75: normalizedScoreSchema,
|
|
72
|
+
p90: normalizedScoreSchema,
|
|
73
|
+
});
|
|
74
|
+
export const scoreDirectionSchema = z.enum(['maximize', 'minimize']);
|
|
75
|
+
export const scaleStrategySchema = z.enum([
|
|
76
|
+
'quantile',
|
|
77
|
+
'binary',
|
|
78
|
+
'uniform',
|
|
79
|
+
'log',
|
|
80
|
+
'step',
|
|
81
|
+
'categorical',
|
|
82
|
+
'percentile_rank',
|
|
83
|
+
]);
|
|
84
|
+
export const scoreColorBandSchema = z.enum(['excellent', 'good', 'adequate', 'poor', 'failing']);
|
|
85
|
+
export const degradationStateSchema = z.object({
|
|
86
|
+
lastRun: z.string(),
|
|
87
|
+
breaches: z.record(z.string(), z.int().nonnegative()),
|
|
88
|
+
});
|
|
89
|
+
export const calibrationStateSchema = z.object({
|
|
90
|
+
lastCalibrated: z.string(),
|
|
91
|
+
distributions: z.record(z.string(), z.object({
|
|
92
|
+
distribution: percentileDistributionSchema,
|
|
93
|
+
sampleSize: z.int().nonnegative(),
|
|
94
|
+
windowStart: z.string(),
|
|
95
|
+
windowEnd: z.string(),
|
|
96
|
+
})),
|
|
97
|
+
psiValues: z.record(z.string(), z.number()).optional(),
|
|
98
|
+
rawScores: z.record(z.string(), z.array(z.number())).optional(),
|
|
99
|
+
});
|
|
100
|
+
export const psiResultSchema = z.object({
|
|
101
|
+
psi: z.number().nonnegative(),
|
|
102
|
+
drifted: z.boolean(),
|
|
103
|
+
});
|
|
104
|
+
export const backtestConfigSchema = z.object({
|
|
105
|
+
varianceThreshold: z.number().positive(),
|
|
106
|
+
coverageDropoutThreshold: normalizedScoreSchema,
|
|
107
|
+
latencySkewThreshold: z.number().positive(),
|
|
108
|
+
confirmationWindow: z.int().positive(),
|
|
109
|
+
ewmaLambda: normalizedScoreSchema.optional().describe('EWMA smoothing factor for drift detection (0–1). Default: EWMA_LAMBDA'),
|
|
110
|
+
stabilityThreshold: z.number().positive().optional().describe('Number of MAD multiples above baseline that constitute drift'),
|
|
111
|
+
});
|
|
112
|
+
export const backtestSnapshotSchema = z.object({
|
|
113
|
+
currentStdDev: z.number().nonnegative(),
|
|
114
|
+
baselineStdDev: z.number().nonnegative(),
|
|
115
|
+
coverageGapCount: z.int().nonnegative(),
|
|
116
|
+
totalCoverageCells: z.int().nonnegative(),
|
|
117
|
+
latencyP95: z.number().nonnegative().optional(),
|
|
118
|
+
latencyP50: z.number().nonnegative().optional(),
|
|
119
|
+
});
|
|
120
|
+
export const taprMetricsSchema = z.object({
|
|
121
|
+
precision: normalizedScoreSchema,
|
|
122
|
+
recall: normalizedScoreSchema,
|
|
123
|
+
f1: normalizedScoreSchema,
|
|
124
|
+
detectionDelay: z.number().nonnegative(),
|
|
125
|
+
});
|
|
126
|
+
export const degradationBacktestResultSchema = z.object({
|
|
127
|
+
config: backtestConfigSchema,
|
|
128
|
+
truePositives: z.int().nonnegative(),
|
|
129
|
+
falsePositives: z.int().nonnegative(),
|
|
130
|
+
falseNegatives: z.int().nonnegative(),
|
|
131
|
+
trueNegatives: z.int().nonnegative(),
|
|
132
|
+
tapr: taprMetricsSchema,
|
|
133
|
+
pointPrecision: normalizedScoreSchema,
|
|
134
|
+
pointRecall: normalizedScoreSchema,
|
|
135
|
+
pointF1: normalizedScoreSchema,
|
|
136
|
+
});
|
|
137
|
+
export const backtestSweepResultSchema = z.object({
|
|
138
|
+
results: z.array(degradationBacktestResultSchema),
|
|
139
|
+
bestByF1: degradationBacktestResultSchema,
|
|
140
|
+
bestByRecall: degradationBacktestResultSchema,
|
|
141
|
+
currentConfigResult: degradationBacktestResultSchema,
|
|
142
|
+
});
|
|
143
|
+
export const ahpComparisonSchema = z.object({
|
|
144
|
+
metricA: z.string().min(1),
|
|
145
|
+
metricB: z.string().min(1),
|
|
146
|
+
preference: z.enum(['1', '3', '5', '7', '9']).transform((v) => parseInt(v)),
|
|
147
|
+
preferred: z.enum(['A', 'B']),
|
|
148
|
+
}).refine((data) => data.metricA < data.metricB, {
|
|
149
|
+
error: 'metricA must be less than metricB (alphabetically sorted)'
|
|
150
|
+
});
|
|
151
|
+
export const tunedCQIWeightsSchema = z.object({
|
|
152
|
+
featureVersion: z.string(),
|
|
153
|
+
weights: z.record(z.string(), normalizedScoreSchema),
|
|
154
|
+
ahpWeights: z.record(z.string(), z.number().nonnegative()),
|
|
155
|
+
criticWeights: z.record(z.string(), z.number().nonnegative()),
|
|
156
|
+
alpha: z.number(),
|
|
157
|
+
consistencyRatio: z.number(),
|
|
158
|
+
incidentCorrelations: z.record(z.string(), z.number()),
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Compute empirical CDF position for a value within a percentile distribution.
|
|
162
|
+
* Returns 0-1 representing where the value falls in the empirical distribution.
|
|
163
|
+
* Uses linear interpolation between known percentile points.
|
|
164
|
+
*/
|
|
165
|
+
export function empiricalCDF(value, dist) {
|
|
166
|
+
if (!Number.isFinite(value))
|
|
167
|
+
return QUANTILE_P50;
|
|
168
|
+
const points = [
|
|
169
|
+
[dist.p10, QUANTILE_P10],
|
|
170
|
+
[dist.p25, QUANTILE_P25],
|
|
171
|
+
[dist.p50, QUANTILE_P50],
|
|
172
|
+
[dist.p75, QUANTILE_P75],
|
|
173
|
+
[dist.p90, QUANTILE_P90],
|
|
174
|
+
];
|
|
175
|
+
// Below p10
|
|
176
|
+
if (value <= dist.p10)
|
|
177
|
+
return Math.max(0, QUANTILE_P10 * (value / Math.max(dist.p10, NUMERICAL_MIN_DENOMINATOR)));
|
|
178
|
+
// Above p90
|
|
179
|
+
if (value >= dist.p90)
|
|
180
|
+
return Math.min(1, QUANTILE_P90 + QUANTILE_P10 * Math.min(1, (value - dist.p90) / Math.max(1 - dist.p90, NUMERICAL_MIN_DENOMINATOR)));
|
|
181
|
+
// Linear interpolation between adjacent percentile points
|
|
182
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
183
|
+
const [v0, p0] = points[i];
|
|
184
|
+
const [v1, p1] = points[i + 1];
|
|
185
|
+
if (value >= v0 && value <= v1) {
|
|
186
|
+
const range = v1 - v0;
|
|
187
|
+
if (range === 0)
|
|
188
|
+
return p0;
|
|
189
|
+
return p0 + (p1 - p0) * ((value - v0) / range);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return QUANTILE_P50; // Fallback
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Minimum sample size for quantile-based scaling to be considered valid.
|
|
196
|
+
* Below this threshold, quantile scaling falls back to uniform even when
|
|
197
|
+
* distribution data is provided (insufficient data for reliable percentiles).
|
|
198
|
+
*/
|
|
199
|
+
export const MIN_QUANTILE_SAMPLE_SIZE = 100;
|
|
200
|
+
/**
|
|
201
|
+
* Adaptive score color band using per-metric scaling strategies.
|
|
202
|
+
* Falls back to uniform scoreColorBand when distribution data is unavailable.
|
|
203
|
+
*
|
|
204
|
+
* @param value - Raw score value
|
|
205
|
+
* @param metric - Metric name (used to look up ScaleStrategy)
|
|
206
|
+
* @param direction - Score direction (maximize or minimize)
|
|
207
|
+
* @param distribution - Optional empirical percentile distribution for quantile scaling
|
|
208
|
+
* @param sampleSize - Number of samples used to compute distribution; quantile requires >= 100
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* adaptiveScoreColorBand(0.92, 'relevance'); // 'excellent' (uniform, maximize)
|
|
212
|
+
* adaptiveScoreColorBand(0.3, 'hallucination', 'minimize'); // 'excellent' (low hallucination is good)
|
|
213
|
+
* adaptiveScoreColorBand(0.5, 'coherence', 'maximize', dist, 200); // quantile-scaled band
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function adaptiveScoreColorBand(value, metric, direction = 'maximize', distribution, sampleSize) {
|
|
217
|
+
const strategy = METRIC_SCALE_STRATEGY[metric] ?? 'uniform';
|
|
218
|
+
switch (strategy) {
|
|
219
|
+
case 'quantile': {
|
|
220
|
+
if (!distribution || (sampleSize !== undefined && sampleSize < MIN_QUANTILE_SAMPLE_SIZE)) {
|
|
221
|
+
return scoreColorBand(value, direction);
|
|
222
|
+
}
|
|
223
|
+
const rank = empiricalCDF(value, distribution);
|
|
224
|
+
return scoreColorBand(rank, 'maximize');
|
|
225
|
+
}
|
|
226
|
+
case 'log': {
|
|
227
|
+
const clamped = Math.max(value, NUMERICAL_MIN_DENOMINATOR);
|
|
228
|
+
const logNorm = Math.min(1, -Math.log10(clamped) / LOG_SCALE_DIVISOR);
|
|
229
|
+
return scoreColorBand(logNorm, 'maximize');
|
|
230
|
+
}
|
|
231
|
+
case 'binary':
|
|
232
|
+
return value >= BINARY_SCALE_EXCELLENT_THRESHOLD ? 'excellent' : 'failing';
|
|
233
|
+
case 'step':
|
|
234
|
+
// Rule-based: binary pass/fail; LLM-based: standard 5-band
|
|
235
|
+
return value >= STEP_SCALE_EXCELLENT_THRESHOLD ? 'excellent' : value >= STEP_SCALE_ADEQUATE_THRESHOLD ? 'adequate' : 'failing';
|
|
236
|
+
case 'categorical':
|
|
237
|
+
// Display as fraction; color by standard bands
|
|
238
|
+
return scoreColorBand(value, direction);
|
|
239
|
+
case 'percentile_rank':
|
|
240
|
+
// Without distribution data, fall back to uniform
|
|
241
|
+
if (!distribution)
|
|
242
|
+
return scoreColorBand(value, direction);
|
|
243
|
+
return scoreColorBand(empiricalCDF(value, distribution), 'maximize');
|
|
244
|
+
case 'uniform':
|
|
245
|
+
default:
|
|
246
|
+
return scoreColorBand(value, direction);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Metrics where lower scores are better. Used as fallback when alert
|
|
251
|
+
* configuration is unavailable for direction inference.
|
|
252
|
+
*/
|
|
253
|
+
export const MINIMIZE_METRICS = new Set([
|
|
254
|
+
'hallucination',
|
|
255
|
+
'evaluation_latency',
|
|
256
|
+
'error_rate',
|
|
257
|
+
'cost',
|
|
258
|
+
]);
|
|
259
|
+
/**
|
|
260
|
+
* Infer ScoreDirection from a metric's alert configuration.
|
|
261
|
+
* Metrics whose primary alert fires on 'below' are 'maximize' metrics.
|
|
262
|
+
* Metrics whose primary alert fires on 'above' are 'minimize' metrics.
|
|
263
|
+
*/
|
|
264
|
+
export function inferScoreDirection(alertDirection) {
|
|
265
|
+
return alertDirection === 'above' ? 'minimize' : 'maximize';
|
|
266
|
+
}
|
|
267
|
+
/** Known label-to-ordinal lookup table */
|
|
268
|
+
const LABEL_ORDINAL_MAP = {
|
|
269
|
+
// Ordinal 4 - Excellent (Pass)
|
|
270
|
+
excellent: { ordinal: 4, category: 'Pass' },
|
|
271
|
+
highly_relevant: { ordinal: 4, category: 'Pass' },
|
|
272
|
+
fully_faithful: { ordinal: 4, category: 'Pass' },
|
|
273
|
+
perfect: { ordinal: 4, category: 'Pass' },
|
|
274
|
+
// Ordinal 3 - Good (Pass)
|
|
275
|
+
relevant: { ordinal: 3, category: 'Pass' },
|
|
276
|
+
good: { ordinal: 3, category: 'Pass' },
|
|
277
|
+
faithful: { ordinal: 3, category: 'Pass' },
|
|
278
|
+
pass: { ordinal: 3, category: 'Pass' },
|
|
279
|
+
correct: { ordinal: 3, category: 'Pass' },
|
|
280
|
+
coherent: { ordinal: 3, category: 'Pass' },
|
|
281
|
+
// Ordinal 2 - Review
|
|
282
|
+
partial: { ordinal: 2, category: 'Review' },
|
|
283
|
+
borderline: { ordinal: 2, category: 'Review' },
|
|
284
|
+
adequate: { ordinal: 2, category: 'Review' },
|
|
285
|
+
mixed: { ordinal: 2, category: 'Review' },
|
|
286
|
+
// Ordinal 1 - Fail
|
|
287
|
+
off_topic: { ordinal: 1, category: 'Fail' },
|
|
288
|
+
irrelevant: { ordinal: 1, category: 'Fail' },
|
|
289
|
+
unfaithful: { ordinal: 1, category: 'Fail' },
|
|
290
|
+
fail: { ordinal: 1, category: 'Fail' },
|
|
291
|
+
incorrect: { ordinal: 1, category: 'Fail' },
|
|
292
|
+
incoherent: { ordinal: 1, category: 'Fail' },
|
|
293
|
+
// Ordinal 0 - Critical Fail
|
|
294
|
+
hallucinated: { ordinal: 0, category: 'Fail' },
|
|
295
|
+
fabricated: { ordinal: 0, category: 'Fail' },
|
|
296
|
+
toxic: { ordinal: 0, category: 'Fail' },
|
|
297
|
+
dangerous: { ordinal: 0, category: 'Fail' },
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Normalize a label for lookup: lowercase, replace hyphens with underscores.
|
|
301
|
+
*/
|
|
302
|
+
function normalizeLabel(label) {
|
|
303
|
+
return label.toLowerCase().replace(/-/g, '_').trim();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Convert a free-text score label to an ordinal value for sorting/filtering.
|
|
307
|
+
* Unmapped labels default to ordinal 2 ("Review") with mapped=false.
|
|
308
|
+
*
|
|
309
|
+
* @param label - Free-text score label from EvaluationResult.scoreLabel
|
|
310
|
+
* @returns Ordinal encoding with category and mapping status
|
|
311
|
+
*/
|
|
312
|
+
export function labelToOrdinal(label) {
|
|
313
|
+
const normalized = normalizeLabel(label);
|
|
314
|
+
const entry = LABEL_ORDINAL_MAP[normalized];
|
|
315
|
+
if (entry) {
|
|
316
|
+
return { ordinal: entry.ordinal, category: entry.category, mapped: true };
|
|
317
|
+
}
|
|
318
|
+
// Default: unmapped labels are "Review" with ordinal 2
|
|
319
|
+
return { ordinal: 2, category: 'Review', mapped: false };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get the label filter category for a given ordinal value.
|
|
323
|
+
*/
|
|
324
|
+
export function ordinalToCategory(ordinal) {
|
|
325
|
+
if (ordinal >= 3)
|
|
326
|
+
return 'Pass';
|
|
327
|
+
if (ordinal === 2)
|
|
328
|
+
return 'Review';
|
|
329
|
+
return 'Fail';
|
|
330
|
+
}
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Composite Quality Index (Section 16.3)
|
|
333
|
+
// ============================================================================
|
|
334
|
+
/** CQI per-metric contribution breakdown */
|
|
335
|
+
export const cqiContributionSchema = z.object({
|
|
336
|
+
metric: z.string(),
|
|
337
|
+
rawScore: z.number(),
|
|
338
|
+
normalizedScore: z.number(),
|
|
339
|
+
weight: z.number(),
|
|
340
|
+
contribution: z.number(),
|
|
341
|
+
});
|
|
342
|
+
/** Composite Quality Index result */
|
|
343
|
+
export const compositeQualityIndexSchema = z.object({
|
|
344
|
+
/** Feature version for historical comparison validity */
|
|
345
|
+
featureVersion: z.string(),
|
|
346
|
+
/** Weighted composite value (0-1) */
|
|
347
|
+
value: normalizedScoreSchema,
|
|
348
|
+
/** Weight configuration used */
|
|
349
|
+
weights: z.record(z.string(), z.number()),
|
|
350
|
+
/** Per-metric breakdown */
|
|
351
|
+
contributions: z.array(cqiContributionSchema),
|
|
352
|
+
});
|
|
353
|
+
// ============================================================================
|
|
354
|
+
// CQI Weight Constants
|
|
355
|
+
// ============================================================================
|
|
356
|
+
const CQI_WEIGHT_RELEVANCE = 0.25;
|
|
357
|
+
const CQI_WEIGHT_FAITHFULNESS = 0.20;
|
|
358
|
+
const CQI_WEIGHT_HALLUCINATION = 0.20;
|
|
359
|
+
const CQI_WEIGHT_TASK_COMPLETION = 0.15;
|
|
360
|
+
const CQI_WEIGHT_COHERENCE = 0.10;
|
|
361
|
+
const CQI_WEIGHT_TOOL_CORRECTNESS = 0.05;
|
|
362
|
+
const CQI_WEIGHT_EVALUATION_LATENCY = 0.05;
|
|
363
|
+
/** Default CQI weights per design doc Section 16.3 */
|
|
364
|
+
export const DEFAULT_CQI_WEIGHTS = {
|
|
365
|
+
relevance: CQI_WEIGHT_RELEVANCE,
|
|
366
|
+
faithfulness: CQI_WEIGHT_FAITHFULNESS,
|
|
367
|
+
hallucination: CQI_WEIGHT_HALLUCINATION,
|
|
368
|
+
task_completion: CQI_WEIGHT_TASK_COMPLETION,
|
|
369
|
+
coherence: CQI_WEIGHT_COHERENCE,
|
|
370
|
+
tool_correctness: CQI_WEIGHT_TOOL_CORRECTNESS,
|
|
371
|
+
evaluation_latency: CQI_WEIGHT_EVALUATION_LATENCY,
|
|
372
|
+
};
|
|
373
|
+
/** Current feature version for CQI */
|
|
374
|
+
const CQI_FEATURE_VERSION = '1.0';
|
|
375
|
+
/**
|
|
376
|
+
* Compute Composite Quality Index from metric results.
|
|
377
|
+
* Direction-normalizes scores before weighting (minimize metrics are inverted).
|
|
378
|
+
* Metrics with no data are excluded; weights are renormalized over available metrics.
|
|
379
|
+
*
|
|
380
|
+
* @param metrics - Quality metric results from dashboard summary
|
|
381
|
+
* @param weights - Optional custom weights (default: DEFAULT_CQI_WEIGHTS)
|
|
382
|
+
* @returns CQI result, or undefined if no metrics have data
|
|
383
|
+
* @example
|
|
384
|
+
* ```ts
|
|
385
|
+
* const cqi = computeCQI(dashboardSummary.metrics);
|
|
386
|
+
* // { score: 0.82, band: 'good', contributions: [...], featureVersion: '1.0' }
|
|
387
|
+
*
|
|
388
|
+
* // Custom weights emphasizing relevance
|
|
389
|
+
* const custom = computeCQI(metrics, { relevance: 0.4, faithfulness: 0.3, coherence: 0.3 });
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
export function computeCQI(metrics, weights) {
|
|
393
|
+
const w = weights ?? DEFAULT_CQI_WEIGHTS;
|
|
394
|
+
const contributions = [];
|
|
395
|
+
let totalWeight = 0;
|
|
396
|
+
for (const metric of metrics) {
|
|
397
|
+
const weight = w[metric.name];
|
|
398
|
+
if (weight === undefined || weight <= 0)
|
|
399
|
+
continue;
|
|
400
|
+
// Use avg as the primary aggregation for CQI
|
|
401
|
+
const rawScore = metric.values.avg;
|
|
402
|
+
if (rawScore === null)
|
|
403
|
+
continue;
|
|
404
|
+
// Direction-normalize: minimize metrics (hallucination, eval_latency) are inverted
|
|
405
|
+
const direction = inferScoreDirection(metric.alerts[0]?.direction ?? (MINIMIZE_METRICS.has(metric.name) ? 'above' : 'below'));
|
|
406
|
+
let normalizedScore;
|
|
407
|
+
if (direction === 'minimize') {
|
|
408
|
+
// For latency (range 0-LATENCY_NORMALIZATION_MAX_SECONDS), normalize to 0-1 first
|
|
409
|
+
if (metric.name === 'evaluation_latency') {
|
|
410
|
+
normalizedScore = Math.max(0, 1 - rawScore / LATENCY_NORMALIZATION_MAX_SECONDS);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
normalizedScore = 1 - rawScore;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
normalizedScore = rawScore;
|
|
418
|
+
}
|
|
419
|
+
normalizedScore = Math.max(0, Math.min(1, normalizedScore));
|
|
420
|
+
totalWeight += weight;
|
|
421
|
+
contributions.push({
|
|
422
|
+
metric: metric.name,
|
|
423
|
+
rawScore: roundTo(rawScore, SCORE_PRECISION),
|
|
424
|
+
normalizedScore: roundTo(normalizedScore, SCORE_PRECISION),
|
|
425
|
+
weight,
|
|
426
|
+
contribution: 0, // placeholder, computed after renormalization
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (contributions.length === 0 || totalWeight === 0)
|
|
430
|
+
return undefined;
|
|
431
|
+
// Renormalize weights and compute contributions so they sum to rawValue
|
|
432
|
+
for (const c of contributions) {
|
|
433
|
+
const normalizedWeight = c.weight / totalWeight;
|
|
434
|
+
c.contribution = roundTo(c.normalizedScore * normalizedWeight, SCORE_PRECISION);
|
|
435
|
+
}
|
|
436
|
+
const rawValue = contributions.reduce((sum, c) => sum + c.contribution, 0);
|
|
437
|
+
return {
|
|
438
|
+
featureVersion: CQI_FEATURE_VERSION,
|
|
439
|
+
value: roundTo(rawValue, SCORE_PRECISION),
|
|
440
|
+
weights: w,
|
|
441
|
+
contributions,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Compute CQI sensitivity by perturbing each weight +/- delta.
|
|
446
|
+
* Reports the CQI range when each weight varies independently.
|
|
447
|
+
*
|
|
448
|
+
* Only metrics with positive base weights are analyzed. Perturbed weights are
|
|
449
|
+
* not renormalized — computeCQI handles renormalization internally, so the
|
|
450
|
+
* sensitivity reflects the effect of shifting one weight's relative importance.
|
|
451
|
+
*
|
|
452
|
+
* @param metrics - Quality metric results
|
|
453
|
+
* @param weights - Base weights (default: DEFAULT_CQI_WEIGHTS)
|
|
454
|
+
* @param delta - Perturbation amount (default: 0.05)
|
|
455
|
+
* @returns Sensitivity analysis, or undefined if CQI cannot be computed
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* const sens = computeCQISensitivity(metrics);
|
|
459
|
+
* // { baseValue: 0.82, delta: 0.05, entries: [
|
|
460
|
+
* // { metric: 'relevance', baseWeight: 0.25, low: 0.80, high: 0.84, range: 0.04 },
|
|
461
|
+
* // ...
|
|
462
|
+
* // ]}
|
|
463
|
+
* ```
|
|
464
|
+
*/
|
|
465
|
+
export function computeCQISensitivity(metrics, weights, delta = DEFAULT_SENSITIVITY_DELTA) {
|
|
466
|
+
const baseWeights = weights ?? DEFAULT_CQI_WEIGHTS;
|
|
467
|
+
const baseCQI = computeCQI(metrics, baseWeights);
|
|
468
|
+
if (!baseCQI)
|
|
469
|
+
return undefined;
|
|
470
|
+
const entries = pipe(Object.entries(baseWeights), filter(([, weight]) => weight > 0), map(([key, weight]) => {
|
|
471
|
+
const lowWeights = { ...baseWeights, [key]: Math.max(0, weight - delta) };
|
|
472
|
+
const highWeights = { ...baseWeights, [key]: weight + delta };
|
|
473
|
+
const lowCQI = computeCQI(metrics, lowWeights);
|
|
474
|
+
const highCQI = computeCQI(metrics, highWeights);
|
|
475
|
+
const low = lowCQI?.value ?? baseCQI.value;
|
|
476
|
+
const high = highCQI?.value ?? baseCQI.value;
|
|
477
|
+
return {
|
|
478
|
+
metric: key,
|
|
479
|
+
baseWeight: weight,
|
|
480
|
+
low: roundTo(Math.min(low, high), SCORE_PRECISION),
|
|
481
|
+
high: roundTo(Math.max(low, high), SCORE_PRECISION),
|
|
482
|
+
range: roundTo(Math.abs(high - low), SCORE_PRECISION),
|
|
483
|
+
};
|
|
484
|
+
}), sort((a, b) => b.range - a.range));
|
|
485
|
+
return {
|
|
486
|
+
baseValue: baseCQI.value,
|
|
487
|
+
delta,
|
|
488
|
+
entries,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const DYNAMICS_FEATURE_VERSION = '1.1';
|
|
492
|
+
/** Default exponential smoothing factor for velocity noise suppression */
|
|
493
|
+
const EMA_ALPHA = 0.3;
|
|
494
|
+
/**
|
|
495
|
+
* Apply exponential moving average to a sequence of values.
|
|
496
|
+
* More recent values receive higher weight (alpha).
|
|
497
|
+
*
|
|
498
|
+
* @param values - Time-ordered values (oldest first)
|
|
499
|
+
* @param alpha - Smoothing factor (0-1). Higher = less smoothing.
|
|
500
|
+
* @returns Smoothed values array of same length
|
|
501
|
+
*/
|
|
502
|
+
function exponentialSmooth(values, alpha) {
|
|
503
|
+
if (values.length === 0)
|
|
504
|
+
return [];
|
|
505
|
+
const result = [values[0]];
|
|
506
|
+
for (let i = 1; i < values.length; i++) {
|
|
507
|
+
result.push(alpha * values[i] + (1 - alpha) * result[i - 1]);
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Compute metric dynamics from consecutive trend snapshots.
|
|
513
|
+
* When trendHistory is provided, applies exponential smoothing (alpha=0.3)
|
|
514
|
+
* to suppress noise amplification from second derivatives.
|
|
515
|
+
*
|
|
516
|
+
* @param currentTrend - Current period trend
|
|
517
|
+
* @param previousTrend - Previous period trend (for acceleration)
|
|
518
|
+
* @param periodHours - Length of each period in hours
|
|
519
|
+
* @param thresholds - Alert thresholds for breach projection
|
|
520
|
+
* @param trendHistory - Optional array of historical trends (oldest first) for EMA smoothing
|
|
521
|
+
* @returns Metric dynamics, or undefined if insufficient data
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* const dynamics = computeMetricDynamics(
|
|
525
|
+
* { currentValue: 0.85, previousValue: 0.80, delta: 0.05 },
|
|
526
|
+
* { currentValue: 0.80, previousValue: 0.78, delta: 0.02 },
|
|
527
|
+
* 1, // 1-hour periods
|
|
528
|
+
* [{ value: 0.7, direction: 'below', severity: 'warning' }]
|
|
529
|
+
* );
|
|
530
|
+
* // { velocity: 0.05, acceleration: 0.03, confidence: 0.5, projectedStatus: 'healthy', ... }
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
export function computeMetricDynamics(currentTrend, previousTrend, periodHours, thresholds, trendHistory) {
|
|
534
|
+
if (periodHours < MIN_PERIOD_HOURS)
|
|
535
|
+
return undefined;
|
|
536
|
+
let velocity;
|
|
537
|
+
let acceleration = 0;
|
|
538
|
+
let inflectionDetected = false;
|
|
539
|
+
if (trendHistory && trendHistory.length >= 2) {
|
|
540
|
+
// Apply exponential smoothing to the velocity series for noise suppression
|
|
541
|
+
const rawVelocities = trendHistory.map(t => t.delta / periodHours);
|
|
542
|
+
const smoothed = exponentialSmooth(rawVelocities, EMA_ALPHA);
|
|
543
|
+
velocity = smoothed[smoothed.length - 1];
|
|
544
|
+
// Acceleration from last two smoothed velocities
|
|
545
|
+
if (smoothed.length >= 2) {
|
|
546
|
+
const prevSmoothedVel = smoothed[smoothed.length - 2];
|
|
547
|
+
acceleration = (velocity - prevSmoothedVel) / periodHours;
|
|
548
|
+
inflectionDetected = prevSmoothedVel !== 0 && Math.sign(velocity) !== Math.sign(prevSmoothedVel);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Fallback to raw computation (backward-compatible)
|
|
553
|
+
velocity = currentTrend.delta / periodHours;
|
|
554
|
+
if (previousTrend) {
|
|
555
|
+
const previousVelocity = previousTrend.delta / periodHours;
|
|
556
|
+
acceleration = (velocity - previousVelocity) / periodHours;
|
|
557
|
+
inflectionDetected = previousVelocity !== 0 && Math.sign(velocity) !== Math.sign(previousVelocity);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Confidence: higher with more data points and smoothing
|
|
561
|
+
// 0.275 (1 trend), 0.35 (2 trends), 0.575 (5 trends), caps at DYNAMICS_CONFIDENCE_MAX (11+ trends)
|
|
562
|
+
const dataPoints = trendHistory ? trendHistory.length : (previousTrend ? 2 : 1);
|
|
563
|
+
const confidence = Math.min(DYNAMICS_CONFIDENCE_MAX, DYNAMICS_CONFIDENCE_BASE + DYNAMICS_CONFIDENCE_STEP * dataPoints);
|
|
564
|
+
// Project breach time
|
|
565
|
+
let projectedStatus = 'healthy';
|
|
566
|
+
let projectedBreachTime;
|
|
567
|
+
if (thresholds && velocity !== 0) {
|
|
568
|
+
const currentValue = currentTrend.currentValue;
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
// Find nearest threshold breach
|
|
571
|
+
let minBreachHours = Infinity;
|
|
572
|
+
for (const t of thresholds) {
|
|
573
|
+
// Determine if velocity is moving toward breach
|
|
574
|
+
const distanceToThreshold = t.direction === 'above'
|
|
575
|
+
? t.value - currentValue // Need to go up to breach
|
|
576
|
+
: currentValue - t.value; // Need to go down to breach
|
|
577
|
+
if (distanceToThreshold <= 0) {
|
|
578
|
+
// Already breached
|
|
579
|
+
projectedStatus = t.severity === 'critical' ? 'critical' : 'warning';
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
// Check if velocity is moving toward threshold
|
|
583
|
+
const movingToward = t.direction === 'above' ? velocity > 0 : velocity < 0;
|
|
584
|
+
if (!movingToward)
|
|
585
|
+
continue;
|
|
586
|
+
const hoursToBreak = distanceToThreshold / Math.abs(velocity);
|
|
587
|
+
if (hoursToBreak < minBreachHours) {
|
|
588
|
+
minBreachHours = hoursToBreak;
|
|
589
|
+
projectedStatus = t.severity === 'critical' ? 'critical' : 'warning';
|
|
590
|
+
projectedBreachTime = new Date(now + hoursToBreak * TIME_MS.HOUR).toISOString();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
featureVersion: DYNAMICS_FEATURE_VERSION,
|
|
596
|
+
velocity: roundTo(velocity, SCORE_PRECISION),
|
|
597
|
+
acceleration: roundTo(acceleration, SCORE_PRECISION),
|
|
598
|
+
inflectionDetected,
|
|
599
|
+
projectedStatus,
|
|
600
|
+
projectedBreachTime,
|
|
601
|
+
confidence: roundTo(confidence, SCORE_PRECISION),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const COVERAGE_CONFIDENCE_VERSION = '2.0';
|
|
605
|
+
/**
|
|
606
|
+
* Compute Normalized Shannon Entropy (Pielou's J) for distribution uniformity.
|
|
607
|
+
* Returns 1 for perfectly uniform, 0 for maximally concentrated.
|
|
608
|
+
*
|
|
609
|
+
* @param counts - Array of counts per group (e.g., evaluations per metric category)
|
|
610
|
+
* @returns Normalized Shannon entropy J in [0,1] where 1 = perfectly uniform
|
|
611
|
+
*/
|
|
612
|
+
export function computeNormalizedEntropy(counts) {
|
|
613
|
+
if (counts.length <= 1)
|
|
614
|
+
return 1;
|
|
615
|
+
const total = counts.reduce((s, c) => s + c, 0);
|
|
616
|
+
if (total === 0)
|
|
617
|
+
return 0;
|
|
618
|
+
const k = counts.length;
|
|
619
|
+
const hMax = Math.log2(k);
|
|
620
|
+
let h = 0;
|
|
621
|
+
for (const c of counts) {
|
|
622
|
+
if (c > 0) {
|
|
623
|
+
const p = c / total;
|
|
624
|
+
h -= p * Math.log2(p);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return roundTo(Math.max(0, Math.min(1, h / hMax)), SCORE_PRECISION);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Compute coverage-weighted confidence.
|
|
631
|
+
* Adjusts confidence level based on evaluation distribution uniformity across inputs.
|
|
632
|
+
* A metric with 100 evaluations all from one session is less representative
|
|
633
|
+
* than 100 evaluations across 50 sessions.
|
|
634
|
+
*
|
|
635
|
+
* @param baseConfidence - Base confidence indicator from computeConfidence()
|
|
636
|
+
* @param evaluationsPerInput - Count of evaluations per input (session or trace)
|
|
637
|
+
* @returns Extended confidence with coverage weighting
|
|
638
|
+
*/
|
|
639
|
+
export function computeCoverageWeightedConfidence(baseConfidence, evaluationsPerInput) {
|
|
640
|
+
const coverageUniformity = computeNormalizedEntropy(evaluationsPerInput);
|
|
641
|
+
const effectiveSampleSize = roundTo(baseConfidence.sampleCount * coverageUniformity, SCORE_PRECISION);
|
|
642
|
+
// Recalculate level using effective sample size
|
|
643
|
+
// Minimum COVERAGE_SAMPLE_HIGH_THRESHOLD effective samples for "high" regardless of uniformity
|
|
644
|
+
let adjustedLevel;
|
|
645
|
+
if (effectiveSampleSize < COVERAGE_SAMPLE_LOW_THRESHOLD) {
|
|
646
|
+
adjustedLevel = 'low';
|
|
647
|
+
}
|
|
648
|
+
else if (effectiveSampleSize >= COVERAGE_SAMPLE_HIGH_THRESHOLD && baseConfidence.level !== 'low') {
|
|
649
|
+
adjustedLevel = 'high';
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
adjustedLevel = 'medium';
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
...baseConfidence,
|
|
656
|
+
featureVersion: COVERAGE_CONFIDENCE_VERSION,
|
|
657
|
+
coverageUniformity,
|
|
658
|
+
effectiveSampleSize,
|
|
659
|
+
adjustedLevel,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
// ============================================================================
|
|
663
|
+
// Degradation Signal (Section 16.1)
|
|
664
|
+
// ============================================================================
|
|
665
|
+
/**
|
|
666
|
+
* Variance ratio threshold for "increasing" trend detection.
|
|
667
|
+
* Research R5: shifted from 1.5x (~87% coverage, 13% FP) to 2.0 (~95% coverage, 5% FP).
|
|
668
|
+
* Aligns with Datadog 2-3 sigma default anomaly bounds.
|
|
669
|
+
*/
|
|
670
|
+
export const VARIANCE_INCREASE_THRESHOLD = 2.0;
|
|
671
|
+
/**
|
|
672
|
+
* Variance decrease threshold. Ratios below this indicate decreasing variance.
|
|
673
|
+
* Mirrors VARIANCE_INCREASE_THRESHOLD: values significantly below 1.0 indicate
|
|
674
|
+
* the metric is converging (less noisy than baseline).
|
|
675
|
+
*/
|
|
676
|
+
export const VARIANCE_DECREASE_THRESHOLD = 0.7;
|
|
677
|
+
/**
|
|
678
|
+
* Coverage dropout rate threshold. Rates above this are considered degraded.
|
|
679
|
+
* A 20% gap rate means 1 in 5 coverage cells is missing evaluations.
|
|
680
|
+
*/
|
|
681
|
+
export const COVERAGE_DROPOUT_THRESHOLD = 0.2;
|
|
682
|
+
/** EWMA smoothing factor. lambda=0.1 catches slow drift; higher = more reactive. */
|
|
683
|
+
export const EWMA_LAMBDA = 0.1;
|
|
684
|
+
/** Current feature version for degradation signal */
|
|
685
|
+
const DEGRADATION_FEATURE_VERSION = '1.1';
|
|
686
|
+
/**
|
|
687
|
+
* Compute EWMA (Exponentially Weighted Moving Average) from a time series.
|
|
688
|
+
* EWMA with lambda=0.1 catches slow quality drift that period-over-period deltas miss.
|
|
689
|
+
*
|
|
690
|
+
* @param values - Time-ordered score values (oldest first)
|
|
691
|
+
* @param lambda - Smoothing factor (0-1). Default: EWMA_LAMBDA (0.1)
|
|
692
|
+
* @returns Final EWMA value, or undefined if no values
|
|
693
|
+
*/
|
|
694
|
+
export function computeEWMA(values, lambda = EWMA_LAMBDA) {
|
|
695
|
+
if (values.length === 0)
|
|
696
|
+
return undefined;
|
|
697
|
+
let ewma = values[0];
|
|
698
|
+
for (let i = 1; i < values.length; i++) {
|
|
699
|
+
ewma = lambda * values[i] + (1 - lambda) * ewma;
|
|
700
|
+
}
|
|
701
|
+
return roundTo(ewma, SCORE_PRECISION);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Compute Median Absolute Deviation (MAD) for robust dispersion estimation.
|
|
705
|
+
* MAD is resistant to outliers unlike standard deviation.
|
|
706
|
+
* Scaled by 1.4826 for normal-equivalent consistency.
|
|
707
|
+
*
|
|
708
|
+
* @param values - Numeric values
|
|
709
|
+
* @returns Scaled MAD value, or 0 for insufficient data
|
|
710
|
+
*/
|
|
711
|
+
export function computeMAD(values) {
|
|
712
|
+
if (values.length < 2)
|
|
713
|
+
return 0;
|
|
714
|
+
const mad = ss.medianAbsoluteDeviation(values);
|
|
715
|
+
return roundTo(mad * MAD_CONSISTENCY_FACTOR, SCORE_PRECISION);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Detect EWMA drift by comparing the trailing EWMA to a baseline mean.
|
|
719
|
+
* The baseline is the first 70% of the series; drift is detected when the
|
|
720
|
+
* EWMA (which tracks recent values) deviates from the baseline by > k * MAD.
|
|
721
|
+
* When MAD = 0, falls back to range-based detection (> 10% of range).
|
|
722
|
+
*/
|
|
723
|
+
export function detectEWMADrift(values, lambda = EWMA_LAMBDA, k = 2) {
|
|
724
|
+
if (values.length < EWMA_DRIFT_MIN_VALUES)
|
|
725
|
+
return false;
|
|
726
|
+
const ewma = computeEWMA(values, lambda);
|
|
727
|
+
if (ewma === undefined)
|
|
728
|
+
return false;
|
|
729
|
+
// Use first EWMA_BASELINE_FRACTION of series as baseline
|
|
730
|
+
const baselineEnd = Math.max(EWMA_BASELINE_MIN_POINTS, Math.floor(values.length * EWMA_BASELINE_FRACTION));
|
|
731
|
+
const baseline = values.slice(0, baselineEnd);
|
|
732
|
+
const baselineMean = baseline.reduce((s, v) => s + v, 0) / baseline.length;
|
|
733
|
+
const mad = computeMAD(baseline);
|
|
734
|
+
if (mad === 0) {
|
|
735
|
+
const min = Math.min(...values);
|
|
736
|
+
const max = Math.max(...values);
|
|
737
|
+
const range = max - min;
|
|
738
|
+
return range > 0 && Math.abs(ewma - baselineMean) > EWMA_RANGE_FALLBACK_RATIO * range;
|
|
739
|
+
}
|
|
740
|
+
return Math.abs(ewma - baselineMean) > k * mad;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Compute degradation signal from current and baseline metrics.
|
|
744
|
+
*
|
|
745
|
+
* @param currentStdDev - Current period score standard deviation
|
|
746
|
+
* @param baselineStdDev - Baseline period score standard deviation
|
|
747
|
+
* @param coverageGapCount - Number of missing coverage cells
|
|
748
|
+
* @param totalCoverageCells - Total coverage cells expected
|
|
749
|
+
* @param latencyP95 - Evaluation latency p95 (optional)
|
|
750
|
+
* @param latencyP50 - Evaluation latency p50 (optional)
|
|
751
|
+
* @param options - Additional options for EWMA drift and confirmation windows
|
|
752
|
+
*/
|
|
753
|
+
export function computeDegradationSignal(currentStdDev, baselineStdDev, coverageGapCount, totalCoverageCells, latencyP95, latencyP50, options) {
|
|
754
|
+
// Variance ratio — when baseline had zero variance but current has variance,
|
|
755
|
+
// use VARIANCE_INCREASE_THRESHOLD + 1 to signal a clear spike rather than
|
|
756
|
+
// defaulting to 1 (stable) which masks the transition from stable to unstable.
|
|
757
|
+
let varianceRatio;
|
|
758
|
+
if (currentStdDev !== null && baselineStdDev !== null) {
|
|
759
|
+
if (baselineStdDev > 0) {
|
|
760
|
+
varianceRatio = roundTo(currentStdDev / baselineStdDev, SCORE_PRECISION);
|
|
761
|
+
}
|
|
762
|
+
else if (currentStdDev > 0) {
|
|
763
|
+
varianceRatio = VARIANCE_INCREASE_THRESHOLD + 1;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
varianceRatio = 1;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
varianceRatio = 1;
|
|
771
|
+
}
|
|
772
|
+
// Variance trend (R5: shifted from 1.5x to 2.0 for 5% FP rate)
|
|
773
|
+
let varianceTrend = 'stable';
|
|
774
|
+
if (varianceRatio > VARIANCE_INCREASE_THRESHOLD)
|
|
775
|
+
varianceTrend = 'increasing';
|
|
776
|
+
else if (varianceRatio < VARIANCE_DECREASE_THRESHOLD)
|
|
777
|
+
varianceTrend = 'decreasing';
|
|
778
|
+
// Coverage dropout rate
|
|
779
|
+
const coverageDropoutRate = totalCoverageCells > 0
|
|
780
|
+
? roundTo(coverageGapCount / totalCoverageCells, SCORE_PRECISION)
|
|
781
|
+
: 0;
|
|
782
|
+
// Latency skew ratio
|
|
783
|
+
const latencySkewRatio = (latencyP95 != null && latencyP50 != null && latencyP50 > 0)
|
|
784
|
+
? roundTo(latencyP95 / latencyP50, SCORE_PRECISION)
|
|
785
|
+
: 1;
|
|
786
|
+
// EWMA drift detection
|
|
787
|
+
const ewmaDriftDetected = options?.historicalValues
|
|
788
|
+
? detectEWMADrift(options.historicalValues)
|
|
789
|
+
: false;
|
|
790
|
+
// Predicted status from feature combination
|
|
791
|
+
let predictedStatus = 'healthy';
|
|
792
|
+
const signals = [
|
|
793
|
+
varianceTrend === 'increasing',
|
|
794
|
+
coverageDropoutRate > COVERAGE_DROPOUT_THRESHOLD,
|
|
795
|
+
latencySkewRatio > LATENCY_SKEW_SIGNAL_THRESHOLD,
|
|
796
|
+
];
|
|
797
|
+
const activeSignals = signals.filter(Boolean).length;
|
|
798
|
+
if (activeSignals >= 2)
|
|
799
|
+
predictedStatus = 'critical';
|
|
800
|
+
else if (activeSignals >= 1)
|
|
801
|
+
predictedStatus = 'warning';
|
|
802
|
+
// EWMA drift escalates healthy -> warning (supplementary signal)
|
|
803
|
+
if (ewmaDriftDetected && predictedStatus === 'healthy') {
|
|
804
|
+
predictedStatus = 'warning';
|
|
805
|
+
}
|
|
806
|
+
// Confirmation window: require CONFIRMATION_MIN_CONSECUTIVE_BREACHES+ for confirmed status
|
|
807
|
+
const priorBreaches = options?.priorConsecutiveBreaches ?? 0;
|
|
808
|
+
const currentlyBreaching = predictedStatus !== 'healthy';
|
|
809
|
+
const consecutiveBreaches = currentlyBreaching ? priorBreaches + 1 : 0;
|
|
810
|
+
const confirmed = consecutiveBreaches >= CONFIRMATION_MIN_CONSECUTIVE_BREACHES;
|
|
811
|
+
return {
|
|
812
|
+
featureVersion: DEGRADATION_FEATURE_VERSION,
|
|
813
|
+
varianceTrend,
|
|
814
|
+
varianceRatio,
|
|
815
|
+
coverageDropoutRate,
|
|
816
|
+
latencySkewRatio,
|
|
817
|
+
predictedStatus,
|
|
818
|
+
ewmaDriftDetected,
|
|
819
|
+
consecutiveBreaches,
|
|
820
|
+
confirmed,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const CORRELATION_FEATURE_VERSION = '3.1';
|
|
824
|
+
/**
|
|
825
|
+
* Compute Pearson correlation coefficient between two numeric arrays.
|
|
826
|
+
* Returns 0 for insufficient data (< 3 points) or zero variance.
|
|
827
|
+
*/
|
|
828
|
+
export function computePearsonR(xs, ys) {
|
|
829
|
+
const n = Math.min(xs.length, ys.length);
|
|
830
|
+
if (n < CORRELATION_MIN_POINTS)
|
|
831
|
+
return 0;
|
|
832
|
+
const xSlice = xs.slice(0, n);
|
|
833
|
+
const ySlice = ys.slice(0, n);
|
|
834
|
+
const r = ss.sampleCorrelation(xSlice, ySlice);
|
|
835
|
+
if (!Number.isFinite(r))
|
|
836
|
+
return 0;
|
|
837
|
+
return roundTo(r, SCORE_PRECISION);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Compute p-value for Pearson R using t-distribution approximation.
|
|
841
|
+
* t = r * sqrt((n-2) / (1-r^2)), two-tailed test.
|
|
842
|
+
* Uses the incomplete beta function approximation for t-distribution CDF.
|
|
843
|
+
*/
|
|
844
|
+
export function pearsonPValue(r, n) {
|
|
845
|
+
if (n < CORRELATION_MIN_POINTS)
|
|
846
|
+
return null;
|
|
847
|
+
const absR = Math.abs(r);
|
|
848
|
+
if (absR >= 1)
|
|
849
|
+
return 0;
|
|
850
|
+
if (absR === 0)
|
|
851
|
+
return 1;
|
|
852
|
+
const df = n - 2;
|
|
853
|
+
const t = absR * Math.sqrt(df / (1 - absR * absR));
|
|
854
|
+
// Two-tailed p-value via t-distribution CDF
|
|
855
|
+
const p = 2 * (1 - tCdf(t, df));
|
|
856
|
+
return Math.max(0, Math.min(1, p));
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Apply Benjamini-Hochberg FDR correction to a set of p-values.
|
|
860
|
+
* Returns a boolean array indicating which hypotheses are significant.
|
|
861
|
+
*
|
|
862
|
+
* @param pValues - Array of p-values
|
|
863
|
+
* @param q - FDR threshold (default: 0.05)
|
|
864
|
+
* @returns Boolean array: true = significant at given FDR level
|
|
865
|
+
*/
|
|
866
|
+
export function benjaminiHochberg(pValues, q = DEFAULT_SENSITIVITY_DELTA) {
|
|
867
|
+
const m = pValues.length;
|
|
868
|
+
if (m === 0)
|
|
869
|
+
return [];
|
|
870
|
+
// Sort indices by p-value
|
|
871
|
+
const indexed = pValues.map((p, i) => ({ p, i }));
|
|
872
|
+
indexed.sort((a, b) => a.p - b.p);
|
|
873
|
+
// Find largest k where p_(k) <= k/m * q
|
|
874
|
+
const significant = new Array(m).fill(false);
|
|
875
|
+
let maxK = -1;
|
|
876
|
+
for (let k = 0; k < m; k++) {
|
|
877
|
+
const threshold = ((k + 1) / m) * q;
|
|
878
|
+
if (indexed[k].p <= threshold) {
|
|
879
|
+
maxK = k;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// All hypotheses up to maxK are significant
|
|
883
|
+
for (let k = 0; k <= maxK; k++) {
|
|
884
|
+
significant[indexed[k].i] = true;
|
|
885
|
+
}
|
|
886
|
+
return significant;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Compute Spearman rank correlation coefficient.
|
|
890
|
+
* Converts values to fractional ranks (average rank for ties) then computes Pearson R on ranks.
|
|
891
|
+
* Better than Pearson for bounded [0,1] quality scores that may have non-linear relationships.
|
|
892
|
+
*/
|
|
893
|
+
export function computeSpearmanR(xs, ys) {
|
|
894
|
+
const n = Math.min(xs.length, ys.length);
|
|
895
|
+
if (n < CORRELATION_MIN_POINTS)
|
|
896
|
+
return 0;
|
|
897
|
+
return computePearsonR(fractionalRanks(xs.slice(0, n)), fractionalRanks(ys.slice(0, n)));
|
|
898
|
+
}
|
|
899
|
+
/** Convert values to fractional ranks (1-based, average rank for ties) */
|
|
900
|
+
function fractionalRanks(values) {
|
|
901
|
+
const indexed = values.map((v, i) => ({ v, i }));
|
|
902
|
+
indexed.sort((a, b) => a.v - b.v);
|
|
903
|
+
const ranks = new Array(values.length);
|
|
904
|
+
let i = 0;
|
|
905
|
+
while (i < indexed.length) {
|
|
906
|
+
let j = i;
|
|
907
|
+
while (j < indexed.length && indexed[j].v === indexed[i].v)
|
|
908
|
+
j++;
|
|
909
|
+
const avgRank = (i + j + 1) / 2; // average of 1-based ranks i+1..j
|
|
910
|
+
for (let k = i; k < j; k++)
|
|
911
|
+
ranks[indexed[k].i] = avgRank;
|
|
912
|
+
i = j;
|
|
913
|
+
}
|
|
914
|
+
return ranks;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Compute Cohen's d effect size from a correlation coefficient.
|
|
918
|
+
* d = 2r / sqrt(1 - r^2). Interpretation: small=0.2, medium=0.5, large=0.8.
|
|
919
|
+
*/
|
|
920
|
+
function correlationToCohenD(r) {
|
|
921
|
+
const absR = Math.abs(r);
|
|
922
|
+
if (absR >= 1)
|
|
923
|
+
return Infinity;
|
|
924
|
+
const d = (2 * absR) / Math.sqrt(1 - absR * absR);
|
|
925
|
+
return roundTo(d, SCORE_PRECISION);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Find the best lag (highest |R|) for a pair of time series.
|
|
929
|
+
* Tests lags from 0 to maxLagSteps by shifting seriesB forward.
|
|
930
|
+
*/
|
|
931
|
+
function findBestLag(seriesA, seriesB, maxLagSteps) {
|
|
932
|
+
let bestR = computePearsonR(seriesA, seriesB);
|
|
933
|
+
let bestLag = 0;
|
|
934
|
+
for (let lag = 1; lag <= maxLagSteps; lag++) {
|
|
935
|
+
// Shift B forward by lag: compare A[lag:] with B[0:n-lag]
|
|
936
|
+
const n = Math.min(seriesA.length, seriesB.length) - lag;
|
|
937
|
+
if (n < CORRELATION_MIN_POINTS)
|
|
938
|
+
break;
|
|
939
|
+
const shiftedA = seriesA.slice(lag);
|
|
940
|
+
const shiftedB = seriesB.slice(0, n);
|
|
941
|
+
const r = computePearsonR(shiftedA, shiftedB);
|
|
942
|
+
if (Math.abs(r) > Math.abs(bestR)) {
|
|
943
|
+
bestR = r;
|
|
944
|
+
bestLag = lag;
|
|
945
|
+
}
|
|
946
|
+
// Also test negative lag: A[0:n-lag] with B[lag:]
|
|
947
|
+
const revA = seriesA.slice(0, n);
|
|
948
|
+
const revB = seriesB.slice(lag);
|
|
949
|
+
const rRev = computePearsonR(revA, revB);
|
|
950
|
+
if (Math.abs(rRev) > Math.abs(bestR)) {
|
|
951
|
+
bestR = rRev;
|
|
952
|
+
bestLag = -lag;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return { r: bestR, lagStep: bestLag };
|
|
956
|
+
}
|
|
957
|
+
// ============================================================================
|
|
958
|
+
// R3: Bivariate Granger Causality Test
|
|
959
|
+
// ============================================================================
|
|
960
|
+
const GRANGER_MIN_SAMPLE_SIZE = 30;
|
|
961
|
+
/**
|
|
962
|
+
* Fit an autoregressive (AR) model using OLS and return the sum of squared residuals.
|
|
963
|
+
* y[t] = c + sum(a_i * y[t-i]) + sum(b_j * x[t-j]) + e[t]
|
|
964
|
+
*
|
|
965
|
+
* When xLags is empty, fits a univariate (restricted) model.
|
|
966
|
+
* When xLags is provided, fits a bivariate (unrestricted) model.
|
|
967
|
+
*
|
|
968
|
+
* Uses normal equations: (X'X)^-1 X'y via Gaussian elimination.
|
|
969
|
+
*/
|
|
970
|
+
function fitARResidualSSR(y, yLags, xLags = []) {
|
|
971
|
+
const n = y.length;
|
|
972
|
+
const k = 1 + yLags.length + xLags.length; // intercept + yLag cols + xLag cols
|
|
973
|
+
if (n <= k)
|
|
974
|
+
return 0;
|
|
975
|
+
// Build design matrix X (n x k): [intercept, yLag1..., xLag1...]
|
|
976
|
+
const xData = [];
|
|
977
|
+
for (let i = 0; i < n; i++) {
|
|
978
|
+
const row = [1];
|
|
979
|
+
for (const lagCol of yLags)
|
|
980
|
+
row.push(lagCol[i]);
|
|
981
|
+
for (const lagCol of xLags)
|
|
982
|
+
row.push(lagCol[i]);
|
|
983
|
+
xData.push(row);
|
|
984
|
+
}
|
|
985
|
+
const X = new Matrix(xData);
|
|
986
|
+
const Y = Matrix.columnVector(y);
|
|
987
|
+
// Solve normal equations: (X'X)β = X'y via QR decomposition
|
|
988
|
+
const beta = solve(X, Y, false);
|
|
989
|
+
// Compute SSR = sum((y - Xβ)^2)
|
|
990
|
+
const residuals = X.mmul(beta).sub(Y);
|
|
991
|
+
let ssr = 0;
|
|
992
|
+
for (let i = 0; i < n; i++) {
|
|
993
|
+
const r = residuals.get(i, 0);
|
|
994
|
+
ssr += r * r;
|
|
995
|
+
}
|
|
996
|
+
return ssr;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Compute p-value for F-distribution.
|
|
1000
|
+
* P(F > f) = 1 - CDF(f, d1, d2)
|
|
1001
|
+
*/
|
|
1002
|
+
function fDistPValue(f, d1, d2) {
|
|
1003
|
+
if (f <= 0 || d1 <= 0 || d2 <= 0)
|
|
1004
|
+
return 1;
|
|
1005
|
+
return 1 - fCdf(f, d1, d2);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Run bivariate Granger causality test: does X Granger-cause Y?
|
|
1009
|
+
*
|
|
1010
|
+
* Compares restricted model (Y on own lags) vs unrestricted model (Y on own lags + X lags).
|
|
1011
|
+
* F = ((SSR_r - SSR_u) / p) / (SSR_u / (n - 2p - 1))
|
|
1012
|
+
*
|
|
1013
|
+
* @returns F-statistic and p-value, or undefined if insufficient data
|
|
1014
|
+
*/
|
|
1015
|
+
function grangerFTest(y, x, lags) {
|
|
1016
|
+
const n = Math.min(y.length, x.length);
|
|
1017
|
+
if (n < lags + GRANGER_MIN_SAMPLE_SIZE)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
const effectiveN = n - lags;
|
|
1020
|
+
const yTarget = y.slice(lags, lags + effectiveN);
|
|
1021
|
+
// Build lag matrices
|
|
1022
|
+
const yLagCols = [];
|
|
1023
|
+
const xLagCols = [];
|
|
1024
|
+
for (let lag = 1; lag <= lags; lag++) {
|
|
1025
|
+
yLagCols.push(y.slice(lags - lag, lags - lag + effectiveN));
|
|
1026
|
+
xLagCols.push(x.slice(lags - lag, lags - lag + effectiveN));
|
|
1027
|
+
}
|
|
1028
|
+
const ssrRestricted = fitARResidualSSR(yTarget, yLagCols);
|
|
1029
|
+
const ssrUnrestricted = fitARResidualSSR(yTarget, yLagCols, xLagCols);
|
|
1030
|
+
if (ssrUnrestricted <= 0)
|
|
1031
|
+
return undefined;
|
|
1032
|
+
const p = lags; // number of additional parameters
|
|
1033
|
+
const dfResidual = effectiveN - 2 * p - 1;
|
|
1034
|
+
if (dfResidual <= 0)
|
|
1035
|
+
return undefined;
|
|
1036
|
+
const fStat = ((ssrRestricted - ssrUnrestricted) / p) / (ssrUnrestricted / dfResidual);
|
|
1037
|
+
if (!Number.isFinite(fStat) || fStat < 0)
|
|
1038
|
+
return undefined;
|
|
1039
|
+
const pValue = fDistPValue(fStat, p, dfResidual);
|
|
1040
|
+
return { fStatistic: roundTo(fStat, SCORE_PRECISION), pValue: roundTo(pValue, SCORE_PRECISION) };
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Compute Granger causality for a pair of time series.
|
|
1044
|
+
* Tests both directions (A->B and B->A) using the best lag from correlation analysis.
|
|
1045
|
+
*
|
|
1046
|
+
* @param seriesA - Time-ordered scores for metric A
|
|
1047
|
+
* @param seriesB - Time-ordered scores for metric B
|
|
1048
|
+
* @param lags - Number of lags to use (from correlation lag detection, minimum 1)
|
|
1049
|
+
* @param significanceLevel - P-value threshold (default: 0.05)
|
|
1050
|
+
*/
|
|
1051
|
+
export function computeGrangerCausality(seriesA, seriesB, lags, significanceLevel = DEFAULT_SENSITIVITY_DELTA) {
|
|
1052
|
+
// Cap lags at n/4 to ensure sufficient residual degrees of freedom (Lütkepohl 2005).
|
|
1053
|
+
// With GRANGER_MIN_SAMPLE_SIZE=30, effectiveLags=1 needs n>=31, effectiveLags=2 needs n>=32.
|
|
1054
|
+
const effectiveLags = Math.max(1, Math.min(lags, Math.floor(Math.min(seriesA.length, seriesB.length) / 4)));
|
|
1055
|
+
// Test A -> B (does A Granger-cause B?)
|
|
1056
|
+
const abTest = grangerFTest(seriesB, seriesA, effectiveLags);
|
|
1057
|
+
// Test B -> A (does B Granger-cause A?)
|
|
1058
|
+
const baTest = grangerFTest(seriesA, seriesB, effectiveLags);
|
|
1059
|
+
if (!abTest && !baTest)
|
|
1060
|
+
return undefined;
|
|
1061
|
+
const abSignificant = abTest != null && abTest.pValue < significanceLevel;
|
|
1062
|
+
const baSignificant = baTest != null && baTest.pValue < significanceLevel;
|
|
1063
|
+
let direction = 'none';
|
|
1064
|
+
let bestF;
|
|
1065
|
+
let bestP;
|
|
1066
|
+
if (abSignificant && baSignificant) {
|
|
1067
|
+
if (!abTest || !baTest) {
|
|
1068
|
+
throw new Error('Expected both Granger tests when both are significant');
|
|
1069
|
+
}
|
|
1070
|
+
direction = 'bidirectional';
|
|
1071
|
+
bestF = Math.max(abTest.fStatistic, baTest.fStatistic);
|
|
1072
|
+
bestP = Math.min(abTest.pValue, baTest.pValue);
|
|
1073
|
+
}
|
|
1074
|
+
else if (abSignificant) {
|
|
1075
|
+
if (!abTest) {
|
|
1076
|
+
throw new Error('Expected A->B Granger test when significant');
|
|
1077
|
+
}
|
|
1078
|
+
direction = 'A->B';
|
|
1079
|
+
bestF = abTest.fStatistic;
|
|
1080
|
+
bestP = abTest.pValue;
|
|
1081
|
+
}
|
|
1082
|
+
else if (baSignificant) {
|
|
1083
|
+
if (!baTest) {
|
|
1084
|
+
throw new Error('Expected B->A Granger test when significant');
|
|
1085
|
+
}
|
|
1086
|
+
direction = 'B->A';
|
|
1087
|
+
bestF = baTest.fStatistic;
|
|
1088
|
+
bestP = baTest.pValue;
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
// Neither significant: report the one with lower p-value
|
|
1092
|
+
if (abTest && baTest) {
|
|
1093
|
+
bestF = abTest.pValue < baTest.pValue ? abTest.fStatistic : baTest.fStatistic;
|
|
1094
|
+
bestP = Math.min(abTest.pValue, baTest.pValue);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
const test = abTest ?? baTest;
|
|
1098
|
+
if (!test) {
|
|
1099
|
+
throw new Error('Expected at least one Granger test result');
|
|
1100
|
+
}
|
|
1101
|
+
bestF = test.fStatistic;
|
|
1102
|
+
bestP = test.pValue;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
fStatistic: bestF,
|
|
1107
|
+
pValue: bestP,
|
|
1108
|
+
lags: effectiveLags,
|
|
1109
|
+
direction,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Compute correlation matrix across all metric pairs.
|
|
1114
|
+
* Supports time-lag detection and BH-FDR significance testing.
|
|
1115
|
+
*
|
|
1116
|
+
* Empty metric time series arrays are valid input and result in `pearsonR=0`
|
|
1117
|
+
* for any pair involving that metric (insufficient data for correlation).
|
|
1118
|
+
*
|
|
1119
|
+
* @param metricTimeSeries - Map of metric name to time-ordered score arrays
|
|
1120
|
+
* @param knownToxicCombos - Set of "metricA:metricB" pairs from correlation rules
|
|
1121
|
+
* @param degradedPeriods - Map of metric name to boolean arrays (true = degraded in that period)
|
|
1122
|
+
* @param options - Lag detection and significance testing options
|
|
1123
|
+
* @example
|
|
1124
|
+
* ```ts
|
|
1125
|
+
* const series = new Map([
|
|
1126
|
+
* ['relevance', [0.9, 0.85, 0.88, 0.92]],
|
|
1127
|
+
* ['faithfulness', [0.8, 0.82, 0.79, 0.85]],
|
|
1128
|
+
* ]);
|
|
1129
|
+
* const correlations = computeCorrelationMatrix(series);
|
|
1130
|
+
* // [{ metricA: 'faithfulness', metricB: 'relevance', pearsonR: 0.87, ... }]
|
|
1131
|
+
* ```
|
|
1132
|
+
*/
|
|
1133
|
+
export function computeCorrelationMatrix(metricTimeSeries, knownToxicCombos, degradedPeriods, options) {
|
|
1134
|
+
const metrics = [...metricTimeSeries.keys()].sort();
|
|
1135
|
+
const maxLagSteps = options?.maxLagSteps ?? 0;
|
|
1136
|
+
const lagStepHours = options?.lagStepHours ?? 1;
|
|
1137
|
+
const fdrQ = options?.fdrQ ?? DEFAULT_SENSITIVITY_DELTA;
|
|
1138
|
+
const results = [];
|
|
1139
|
+
// First pass: compute all correlations and p-values
|
|
1140
|
+
const pairData = [];
|
|
1141
|
+
for (let i = 0; i < metrics.length; i++) {
|
|
1142
|
+
for (let j = i + 1; j < metrics.length; j++) {
|
|
1143
|
+
const metricA = metrics[i];
|
|
1144
|
+
const metricB = metrics[j];
|
|
1145
|
+
const seriesA = metricTimeSeries.get(metricA);
|
|
1146
|
+
const seriesB = metricTimeSeries.get(metricB);
|
|
1147
|
+
if (!seriesA || !seriesB) {
|
|
1148
|
+
console.debug(`[computeCorrelationMatrix] skipping pair (${metricA}, ${metricB}): series not found`);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
// Find best lag (or use lag=0 if no lag detection)
|
|
1152
|
+
let pearsonR;
|
|
1153
|
+
let lagStep;
|
|
1154
|
+
if (maxLagSteps > 0) {
|
|
1155
|
+
const best = findBestLag(seriesA, seriesB, maxLagSteps);
|
|
1156
|
+
pearsonR = best.r;
|
|
1157
|
+
lagStep = best.lagStep;
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
pearsonR = computePearsonR(seriesA, seriesB);
|
|
1161
|
+
lagStep = 0;
|
|
1162
|
+
}
|
|
1163
|
+
const effectiveN = Math.min(seriesA.length, seriesB.length) - Math.abs(lagStep);
|
|
1164
|
+
const pValue = pearsonPValue(pearsonR, effectiveN);
|
|
1165
|
+
// Spearman rank correlation (computed at best lag)
|
|
1166
|
+
let spearmanR;
|
|
1167
|
+
if (lagStep === 0) {
|
|
1168
|
+
spearmanR = computeSpearmanR(seriesA, seriesB);
|
|
1169
|
+
}
|
|
1170
|
+
else if (lagStep > 0) {
|
|
1171
|
+
spearmanR = computeSpearmanR(seriesA.slice(lagStep), seriesB.slice(0, effectiveN));
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
spearmanR = computeSpearmanR(seriesA.slice(0, effectiveN), seriesB.slice(-lagStep));
|
|
1175
|
+
}
|
|
1176
|
+
const effectSize = correlationToCohenD(pearsonR);
|
|
1177
|
+
// Co-occurrence rate from degraded periods
|
|
1178
|
+
let coOccurrenceRate = 0;
|
|
1179
|
+
if (degradedPeriods) {
|
|
1180
|
+
const degradedA = degradedPeriods.get(metricA);
|
|
1181
|
+
const degradedB = degradedPeriods.get(metricB);
|
|
1182
|
+
if (degradedA && degradedB) {
|
|
1183
|
+
const n = Math.min(degradedA.length, degradedB.length);
|
|
1184
|
+
if (n > 0) {
|
|
1185
|
+
let coCount = 0;
|
|
1186
|
+
for (let k = 0; k < n; k++) {
|
|
1187
|
+
if (degradedA[k] && degradedB[k])
|
|
1188
|
+
coCount++;
|
|
1189
|
+
}
|
|
1190
|
+
coOccurrenceRate = roundTo(coCount / n, SCORE_PRECISION);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const comboKey = `${metricA}:${metricB}`;
|
|
1195
|
+
const reverseKey = `${metricB}:${metricA}`;
|
|
1196
|
+
const isKnownToxicCombo = knownToxicCombos
|
|
1197
|
+
? (knownToxicCombos.has(comboKey) || knownToxicCombos.has(reverseKey))
|
|
1198
|
+
: false;
|
|
1199
|
+
pairData.push({
|
|
1200
|
+
metricA, metricB, pearsonR, spearmanR, effectSize,
|
|
1201
|
+
lagHours: lagStep * lagStepHours,
|
|
1202
|
+
pValue, coOccurrenceRate, isKnownToxicCombo, effectiveN,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// Second pass: apply BH-FDR significance testing
|
|
1207
|
+
// Filter out pairs with null pValues (insufficient data) before BH-FDR
|
|
1208
|
+
const validIndices = pairData.reduce((acc, d, i) => {
|
|
1209
|
+
if (d.pValue !== null)
|
|
1210
|
+
acc.push(i);
|
|
1211
|
+
return acc;
|
|
1212
|
+
}, []);
|
|
1213
|
+
const validPValues = validIndices.map(i => pairData[i].pValue);
|
|
1214
|
+
const validSignificantFlags = benjaminiHochberg(validPValues, fdrQ);
|
|
1215
|
+
const validIndexMap = new Map(validIndices.map((idx, pos) => [idx, pos]));
|
|
1216
|
+
const significantFlags = pairData.map((d, i) => {
|
|
1217
|
+
if (d.pValue === null)
|
|
1218
|
+
return false;
|
|
1219
|
+
const validPos = validIndexMap.get(i);
|
|
1220
|
+
return validPos !== undefined ? validSignificantFlags[validPos] : false;
|
|
1221
|
+
});
|
|
1222
|
+
// Granger causality config
|
|
1223
|
+
const grangerCfg = options?.grangerConfig;
|
|
1224
|
+
const grangerEnabled = grangerCfg?.enabled === true;
|
|
1225
|
+
const grangerMinN = grangerCfg?.minSampleSize ?? GRANGER_MIN_SAMPLE_SIZE;
|
|
1226
|
+
const grangerAlpha = grangerCfg?.significanceLevel ?? DEFAULT_SENSITIVITY_DELTA;
|
|
1227
|
+
for (let idx = 0; idx < pairData.length; idx++) {
|
|
1228
|
+
const d = pairData[idx];
|
|
1229
|
+
const isSignificant = significantFlags[idx];
|
|
1230
|
+
// Run Granger test on significant pairs when enabled
|
|
1231
|
+
let granger;
|
|
1232
|
+
let causalConfidence = 'correlation';
|
|
1233
|
+
if (grangerEnabled && isSignificant && d.effectiveN >= grangerMinN) {
|
|
1234
|
+
const seriesA = metricTimeSeries.get(d.metricA);
|
|
1235
|
+
const seriesB = metricTimeSeries.get(d.metricB);
|
|
1236
|
+
if (!seriesA || !seriesB) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
// Use fixed lag=GRANGER_FIXED_LAG_ORDER (not correlation-detected lag) to avoid data-snooping
|
|
1240
|
+
// inflation of Type I error. The correlation lag maximizes |R| across
|
|
1241
|
+
// tested offsets; reusing it for AR order would bias the F-test.
|
|
1242
|
+
granger = computeGrangerCausality(seriesA, seriesB, GRANGER_FIXED_LAG_ORDER, grangerAlpha);
|
|
1243
|
+
if (granger && granger.direction !== 'none') {
|
|
1244
|
+
causalConfidence = 'granger';
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
results.push({
|
|
1248
|
+
featureVersion: CORRELATION_FEATURE_VERSION,
|
|
1249
|
+
metricA: d.metricA,
|
|
1250
|
+
metricB: d.metricB,
|
|
1251
|
+
pearsonR: d.pearsonR,
|
|
1252
|
+
spearmanR: d.spearmanR,
|
|
1253
|
+
effectSize: d.effectSize,
|
|
1254
|
+
lagHours: d.lagHours,
|
|
1255
|
+
significant: isSignificant,
|
|
1256
|
+
pValue: d.pValue,
|
|
1257
|
+
causalConfidence,
|
|
1258
|
+
coOccurrenceRate: d.coOccurrenceRate,
|
|
1259
|
+
isKnownToxicCombo: d.isKnownToxicCombo,
|
|
1260
|
+
granger,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
return results;
|
|
1264
|
+
}
|
|
1265
|
+
// ============================================================================
|
|
1266
|
+
// R4: Percentile Distribution & Streaming Digest (Section 16.2)
|
|
1267
|
+
// ============================================================================
|
|
1268
|
+
/**
|
|
1269
|
+
* Compute percentile distribution from an array of numeric scores.
|
|
1270
|
+
* Uses linear interpolation between adjacent sorted values.
|
|
1271
|
+
*
|
|
1272
|
+
* @param scores - Array of numeric scores (unsorted ok, will be sorted internally)
|
|
1273
|
+
* @returns PercentileDistribution with p10/p25/p50/p75/p90, or undefined if < 3 scores
|
|
1274
|
+
*/
|
|
1275
|
+
export function computePercentileDistribution(scores) {
|
|
1276
|
+
if (scores.length < CORRELATION_MIN_POINTS)
|
|
1277
|
+
return undefined;
|
|
1278
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
1279
|
+
return {
|
|
1280
|
+
p10: roundTo(ss.quantileSorted(sorted, QUANTILE_P10), SCORE_PRECISION),
|
|
1281
|
+
p25: roundTo(ss.quantileSorted(sorted, QUANTILE_P25), SCORE_PRECISION),
|
|
1282
|
+
p50: roundTo(ss.quantileSorted(sorted, QUANTILE_P50), SCORE_PRECISION),
|
|
1283
|
+
p75: roundTo(ss.quantileSorted(sorted, QUANTILE_P75), SCORE_PRECISION),
|
|
1284
|
+
p90: roundTo(ss.quantileSorted(sorted, QUANTILE_P90), SCORE_PRECISION),
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Compute the percentile rank of a value within a sorted array.
|
|
1289
|
+
* Returns the fraction of values in the array that are <= the given value (0-1).
|
|
1290
|
+
*
|
|
1291
|
+
* @param value - The value to rank
|
|
1292
|
+
* @param sortedScores - Pre-sorted (ascending) array of scores
|
|
1293
|
+
* @returns Percentile rank (0-1)
|
|
1294
|
+
*/
|
|
1295
|
+
export function computePercentileRank(value, sortedScores) {
|
|
1296
|
+
if (sortedScores.length === 0)
|
|
1297
|
+
return QUANTILE_P50;
|
|
1298
|
+
const n = sortedScores.length;
|
|
1299
|
+
// Binary search for first index where sortedScores[idx] > value (upper bound)
|
|
1300
|
+
let lo = 0, hi = n;
|
|
1301
|
+
while (lo < hi) {
|
|
1302
|
+
const mid = (lo + hi) >>> 1;
|
|
1303
|
+
if (sortedScores[mid] <= value)
|
|
1304
|
+
lo = mid + 1;
|
|
1305
|
+
else
|
|
1306
|
+
hi = mid;
|
|
1307
|
+
}
|
|
1308
|
+
const upper = lo; // count of values <= value
|
|
1309
|
+
// Binary search for first index where sortedScores[idx] >= value (lower bound)
|
|
1310
|
+
lo = 0;
|
|
1311
|
+
hi = n;
|
|
1312
|
+
while (lo < hi) {
|
|
1313
|
+
const mid = (lo + hi) >>> 1;
|
|
1314
|
+
if (sortedScores[mid] < value)
|
|
1315
|
+
lo = mid + 1;
|
|
1316
|
+
else
|
|
1317
|
+
hi = mid;
|
|
1318
|
+
}
|
|
1319
|
+
const lower = lo; // count of values < value
|
|
1320
|
+
// Fractional rank: midpoint of tied range
|
|
1321
|
+
return roundTo((lower + upper) / (2 * n), SCORE_PRECISION);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Streaming percentile estimator backed by DDSketch.
|
|
1325
|
+
* Wraps `@datadog/sketches-js` with a simplified API for quality pipeline use.
|
|
1326
|
+
*/
|
|
1327
|
+
export class StreamingPercentile {
|
|
1328
|
+
sketch;
|
|
1329
|
+
totalCount = 0;
|
|
1330
|
+
constructor() {
|
|
1331
|
+
this.sketch = new DDSketch();
|
|
1332
|
+
}
|
|
1333
|
+
/** Add a single value to the sketch */
|
|
1334
|
+
add(value) {
|
|
1335
|
+
if (!Number.isFinite(value))
|
|
1336
|
+
throw new TypeError(`StreamingPercentile: value must be finite, got ${value}`);
|
|
1337
|
+
this.sketch.accept(value);
|
|
1338
|
+
this.totalCount++;
|
|
1339
|
+
}
|
|
1340
|
+
/** Add multiple values at once */
|
|
1341
|
+
addAll(values) {
|
|
1342
|
+
for (const v of values)
|
|
1343
|
+
this.add(v);
|
|
1344
|
+
}
|
|
1345
|
+
/** Get the current total count of observed values */
|
|
1346
|
+
count() {
|
|
1347
|
+
return this.totalCount;
|
|
1348
|
+
}
|
|
1349
|
+
/** Estimate a percentile (0-100) from the sketch */
|
|
1350
|
+
percentile(p) {
|
|
1351
|
+
if (this.totalCount === 0)
|
|
1352
|
+
return 0;
|
|
1353
|
+
return roundTo(this.sketch.getValueAtQuantile(p / 100), SCORE_PRECISION);
|
|
1354
|
+
}
|
|
1355
|
+
/** Get full percentile distribution */
|
|
1356
|
+
distribution() {
|
|
1357
|
+
if (this.totalCount < CORRELATION_MIN_POINTS)
|
|
1358
|
+
return undefined;
|
|
1359
|
+
return {
|
|
1360
|
+
p10: roundTo(this.sketch.getValueAtQuantile(QUANTILE_P10), SCORE_PRECISION),
|
|
1361
|
+
p25: roundTo(this.sketch.getValueAtQuantile(QUANTILE_P25), SCORE_PRECISION),
|
|
1362
|
+
p50: roundTo(this.sketch.getValueAtQuantile(QUANTILE_P50), SCORE_PRECISION),
|
|
1363
|
+
p75: roundTo(this.sketch.getValueAtQuantile(QUANTILE_P75), SCORE_PRECISION),
|
|
1364
|
+
p90: roundTo(this.sketch.getValueAtQuantile(QUANTILE_P90), SCORE_PRECISION),
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Compute Population Stability Index (PSI) between two distributions.
|
|
1370
|
+
* PSI measures distribution shift: < 0.1 = stable, 0.1–0.25 = moderate, > 0.25 = significant.
|
|
1371
|
+
*
|
|
1372
|
+
* Uses a `bins`-bin histogram comparison between expected (baseline) and actual (current).
|
|
1373
|
+
* Returns { psi: 0, drifted: false } when either array has fewer than MIN_QUANTILE_SAMPLE_SIZE
|
|
1374
|
+
* values (insufficient data for reliable estimation).
|
|
1375
|
+
*/
|
|
1376
|
+
export function computePSI(expected, actual, bins = DEFAULT_BIN_COUNT) {
|
|
1377
|
+
if (expected.length < MIN_QUANTILE_SAMPLE_SIZE || actual.length < MIN_QUANTILE_SAMPLE_SIZE) {
|
|
1378
|
+
return { psi: 0, drifted: false };
|
|
1379
|
+
}
|
|
1380
|
+
const toBinProportions = (values) => {
|
|
1381
|
+
const hist = new Array(bins).fill(0);
|
|
1382
|
+
for (const v of values) {
|
|
1383
|
+
const bin = Math.min(Math.max(Math.floor(v * bins), 0), bins - 1);
|
|
1384
|
+
hist[bin]++;
|
|
1385
|
+
}
|
|
1386
|
+
// PSI_LOG_EPSILON prevents log(0) when a bin is empty
|
|
1387
|
+
return hist.map(c => Math.max(c / values.length, PSI_LOG_EPSILON));
|
|
1388
|
+
};
|
|
1389
|
+
const pExpected = toBinProportions(expected);
|
|
1390
|
+
const pActual = toBinProportions(actual);
|
|
1391
|
+
let psi = 0;
|
|
1392
|
+
for (let i = 0; i < bins; i++) {
|
|
1393
|
+
psi += (pActual[i] - pExpected[i]) * Math.log(pActual[i] / pExpected[i]);
|
|
1394
|
+
}
|
|
1395
|
+
const roundedPsi = roundTo(Math.max(0, psi), SCORE_PRECISION);
|
|
1396
|
+
return {
|
|
1397
|
+
psi: roundedPsi,
|
|
1398
|
+
drifted: roundedPsi > PSI_RECALIBRATION_THRESHOLD,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Determine whether calibration state should be rewritten based on PSI drift.
|
|
1403
|
+
* Returns shouldWrite: true on first run (null previousState) or if any metric drifts.
|
|
1404
|
+
*/
|
|
1405
|
+
export function shouldRecalibrate(previousState, currentScores) {
|
|
1406
|
+
if (previousState === null) {
|
|
1407
|
+
return { shouldWrite: true, psiValues: {} };
|
|
1408
|
+
}
|
|
1409
|
+
if (!previousState.rawScores) {
|
|
1410
|
+
return { shouldWrite: true, psiValues: {} };
|
|
1411
|
+
}
|
|
1412
|
+
const psiValues = {};
|
|
1413
|
+
let shouldWrite = false;
|
|
1414
|
+
for (const metric of Object.keys(currentScores)) {
|
|
1415
|
+
const previous = previousState.rawScores[metric];
|
|
1416
|
+
if (!previous) {
|
|
1417
|
+
shouldWrite = true;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
const result = computePSI(previous, currentScores[metric]);
|
|
1421
|
+
psiValues[metric] = result.psi;
|
|
1422
|
+
if (result.drifted) {
|
|
1423
|
+
shouldWrite = true;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return { shouldWrite, psiValues };
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Compute calibration distributions from per-metric evaluation scores.
|
|
1430
|
+
* Skips metrics with fewer than MIN_QUANTILE_SAMPLE_SIZE scores (unreliable percentiles).
|
|
1431
|
+
* Uses StreamingPercentile to compute p10/p25/p50/p75/p90 for each qualifying metric.
|
|
1432
|
+
* windowStart/windowEnd metadata reflects a CALIBRATION_WINDOW_DAYS rolling window,
|
|
1433
|
+
* but the caller is responsible for pre-filtering scores to that window before passing them in.
|
|
1434
|
+
*/
|
|
1435
|
+
export function computeCalibrationDistributions(evaluationsByMetric) {
|
|
1436
|
+
const distributions = {};
|
|
1437
|
+
const now = new Date().toISOString();
|
|
1438
|
+
const windowStart = new Date(Date.now() - CALIBRATION_WINDOW_DAYS * TIME_MS.DAY).toISOString();
|
|
1439
|
+
for (const [metric, scores] of Object.entries(evaluationsByMetric)) {
|
|
1440
|
+
if (scores.length < MIN_QUANTILE_SAMPLE_SIZE)
|
|
1441
|
+
continue;
|
|
1442
|
+
const dist = computePercentileDistribution(scores);
|
|
1443
|
+
if (!dist)
|
|
1444
|
+
continue;
|
|
1445
|
+
distributions[metric] = {
|
|
1446
|
+
distribution: dist,
|
|
1447
|
+
sampleSize: scores.length,
|
|
1448
|
+
windowStart,
|
|
1449
|
+
windowEnd: now,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return distributions;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Load calibration state from persisted JSON file.
|
|
1456
|
+
* Returns null if file does not exist or is malformed.
|
|
1457
|
+
*/
|
|
1458
|
+
export function loadCalibrationState(dir) {
|
|
1459
|
+
const filePath = join(dir, CALIBRATION_STATE_FILE);
|
|
1460
|
+
try {
|
|
1461
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
1462
|
+
const parsed = JSON.parse(raw);
|
|
1463
|
+
const result = calibrationStateSchema.safeParse(parsed);
|
|
1464
|
+
if (result.success) {
|
|
1465
|
+
return result.data;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
catch { /* file not found or malformed */ }
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Save calibration state to JSON file for cross-run use by the dashboard API.
|
|
1473
|
+
* Validates state before writing to ensure round-trip integrity.
|
|
1474
|
+
*/
|
|
1475
|
+
export function saveCalibrationState(dir, state) {
|
|
1476
|
+
const validated = calibrationStateSchema.parse(state);
|
|
1477
|
+
writeFileSync(join(dir, CALIBRATION_STATE_FILE), JSON.stringify(validated, null, FORMAT_JSON_INDENT_SPACES) + '\n');
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Compute all derived features in correct dependency order.
|
|
1481
|
+
*
|
|
1482
|
+
* Pipeline ordering (QM2):
|
|
1483
|
+
* Step 2 (aggregate) outputs: metrics, CoverageHeatmap
|
|
1484
|
+
* ↓
|
|
1485
|
+
* Step 3a: CQI (depends on metrics)
|
|
1486
|
+
* Step 3b: Coverage-weighted confidence (depends on metrics + CoverageHeatmap via evaluationsPerInput)
|
|
1487
|
+
* Step 3c: Correlation matrix (depends on metricTimeSeries, independent of 3a/3b)
|
|
1488
|
+
*
|
|
1489
|
+
* CoverageHeatmap must be computed before calling this function.
|
|
1490
|
+
* The evaluationsPerInput parameter is derived from CoverageHeatmap output.
|
|
1491
|
+
*
|
|
1492
|
+
* @param input - Aggregated data from Step 2
|
|
1493
|
+
* @returns All derived features
|
|
1494
|
+
*/
|
|
1495
|
+
export function computeDerivedFeatures(input) {
|
|
1496
|
+
// Step 3a: Composite Quality Index (depends only on metrics)
|
|
1497
|
+
const cqi = computeCQI(input.metrics, input.cqiWeights);
|
|
1498
|
+
const cqiSensitivity = cqi ? computeCQISensitivity(input.metrics, input.cqiWeights) : undefined;
|
|
1499
|
+
// Step 3b: Coverage-weighted confidence (depends on metrics + evaluationsPerInput from CoverageHeatmap)
|
|
1500
|
+
const coverageConfidence = new Map();
|
|
1501
|
+
if (input.evaluationsPerInput) {
|
|
1502
|
+
for (const metric of input.metrics) {
|
|
1503
|
+
const perInput = input.evaluationsPerInput.get(metric.name);
|
|
1504
|
+
if (metric.confidence && perInput && perInput.length > 0) {
|
|
1505
|
+
coverageConfidence.set(metric.name, computeCoverageWeightedConfidence(metric.confidence, perInput));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// Step 3c: Correlation matrix (depends on metricTimeSeries, independent of 3a/3b)
|
|
1510
|
+
const correlations = input.metricTimeSeries
|
|
1511
|
+
? computeCorrelationMatrix(input.metricTimeSeries, input.knownToxicCombos, input.degradedPeriods, input.correlationOptions)
|
|
1512
|
+
: [];
|
|
1513
|
+
return { cqi, cqiSensitivity, coverageConfidence, correlations };
|
|
1514
|
+
}
|
|
1515
|
+
/** Parameter grid for sweepDegradationParams — all combinations are evaluated */
|
|
1516
|
+
export const BACKTEST_SWEEP = {
|
|
1517
|
+
varianceThreshold: [1.5, 1.75, 2.0, 2.5, 3.0],
|
|
1518
|
+
coverageDropoutThreshold: [0.1, 0.15, 0.2, 0.3, 0.4],
|
|
1519
|
+
ewmaLambda: [0.05, 0.1, 0.15, 0.2, 0.3],
|
|
1520
|
+
confirmationWindow: [1, 2, 3, 4],
|
|
1521
|
+
stabilityThreshold: [0.005, 0.01, 0.02, 0.03, 0.05],
|
|
1522
|
+
};
|
|
1523
|
+
/** Current production detector configuration used as baseline in sweepDegradationParams */
|
|
1524
|
+
export const CURRENT_PRODUCTION_CONFIG = {
|
|
1525
|
+
varianceThreshold: VARIANCE_INCREASE_THRESHOLD,
|
|
1526
|
+
coverageDropoutThreshold: COVERAGE_DROPOUT_THRESHOLD,
|
|
1527
|
+
latencySkewThreshold: LATENCY_SKEW_SIGNAL_THRESHOLD,
|
|
1528
|
+
ewmaLambda: EWMA_LAMBDA,
|
|
1529
|
+
confirmationWindow: CONFIRMATION_MIN_CONSECUTIVE_BREACHES,
|
|
1530
|
+
};
|
|
1531
|
+
/**
|
|
1532
|
+
* Inject synthetic degradation into baseline snapshots for testing.
|
|
1533
|
+
* Returns a new array with degradation injected at the specified range.
|
|
1534
|
+
*
|
|
1535
|
+
* - variance_spike: sets currentStdDev = baselineStdDev * magnitude
|
|
1536
|
+
* - coverage_gap: sets coverageGapCount = totalCoverageCells * magnitude (fraction)
|
|
1537
|
+
* - latency_jump: sets latencyP95 = latencyP50 * magnitude (creates skew ratio = magnitude)
|
|
1538
|
+
*/
|
|
1539
|
+
export function injectDegradationScenario(snapshots, type, magnitude, startIdx, duration) {
|
|
1540
|
+
return snapshots.map((s, i) => {
|
|
1541
|
+
if (i < startIdx || i >= startIdx + duration)
|
|
1542
|
+
return s;
|
|
1543
|
+
switch (type) {
|
|
1544
|
+
case 'variance_spike':
|
|
1545
|
+
return { ...s, currentStdDev: s.baselineStdDev * magnitude };
|
|
1546
|
+
case 'coverage_gap':
|
|
1547
|
+
return { ...s, coverageGapCount: Math.round(s.totalCoverageCells * magnitude) };
|
|
1548
|
+
case 'latency_jump':
|
|
1549
|
+
if (s.latencyP50 === undefined)
|
|
1550
|
+
return s;
|
|
1551
|
+
return { ...s, latencyP95: s.latencyP50 * magnitude };
|
|
1552
|
+
default:
|
|
1553
|
+
return s;
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
// ============================================================================
|
|
1558
|
+
// Role-Aware Feature Config (Section 16.4)
|
|
1559
|
+
// ============================================================================
|
|
1560
|
+
/** Zod schema for feature visibility configuration per role */
|
|
1561
|
+
export const roleFeatureConfigSchema = z.object({
|
|
1562
|
+
showCQI: z.boolean(),
|
|
1563
|
+
showCQIBreakdown: z.boolean(),
|
|
1564
|
+
showVariance: z.boolean(),
|
|
1565
|
+
showAcceleration: z.boolean(),
|
|
1566
|
+
showProjectedBreach: z.boolean(),
|
|
1567
|
+
showCorrelationRemediation: z.boolean(),
|
|
1568
|
+
showCoverageHeatmap: z.boolean(),
|
|
1569
|
+
showPipelineFunnel: z.boolean(),
|
|
1570
|
+
showProvenance: z.boolean(),
|
|
1571
|
+
showRawExport: z.boolean(),
|
|
1572
|
+
/** Chars before "Show more" truncation in explanations */
|
|
1573
|
+
explanationTruncation: z.number().int().nonnegative(),
|
|
1574
|
+
/** How many worst evaluations to display */
|
|
1575
|
+
maxWorstEvaluations: z.number().int().nonnegative(),
|
|
1576
|
+
});
|
|
1577
|
+
/** Feature configuration per role per design doc Section 16.4 */
|
|
1578
|
+
const configs = {
|
|
1579
|
+
executive: {
|
|
1580
|
+
showCQI: true,
|
|
1581
|
+
showCQIBreakdown: true,
|
|
1582
|
+
showVariance: false,
|
|
1583
|
+
showAcceleration: false,
|
|
1584
|
+
showProjectedBreach: true,
|
|
1585
|
+
showCorrelationRemediation: false,
|
|
1586
|
+
showCoverageHeatmap: false,
|
|
1587
|
+
showPipelineFunnel: false,
|
|
1588
|
+
showProvenance: false,
|
|
1589
|
+
showRawExport: false,
|
|
1590
|
+
explanationTruncation: EXECUTIVE_EXPLANATION_TRUNCATION,
|
|
1591
|
+
maxWorstEvaluations: EXECUTIVE_MAX_WORST_EVALUATIONS,
|
|
1592
|
+
},
|
|
1593
|
+
operator: {
|
|
1594
|
+
showCQI: false,
|
|
1595
|
+
showCQIBreakdown: false,
|
|
1596
|
+
showVariance: true,
|
|
1597
|
+
showAcceleration: true,
|
|
1598
|
+
showProjectedBreach: true,
|
|
1599
|
+
showCorrelationRemediation: true,
|
|
1600
|
+
showCoverageHeatmap: true,
|
|
1601
|
+
showPipelineFunnel: true,
|
|
1602
|
+
showProvenance: false,
|
|
1603
|
+
showRawExport: false,
|
|
1604
|
+
explanationTruncation: TEXT_PREVIEW_LIMIT,
|
|
1605
|
+
maxWorstEvaluations: OPERATOR_MAX_WORST_EVALUATIONS,
|
|
1606
|
+
},
|
|
1607
|
+
auditor: {
|
|
1608
|
+
showCQI: true,
|
|
1609
|
+
showCQIBreakdown: true,
|
|
1610
|
+
showVariance: true,
|
|
1611
|
+
showAcceleration: false,
|
|
1612
|
+
showProjectedBreach: false,
|
|
1613
|
+
showCorrelationRemediation: true,
|
|
1614
|
+
showCoverageHeatmap: true,
|
|
1615
|
+
showPipelineFunnel: true,
|
|
1616
|
+
showProvenance: true,
|
|
1617
|
+
showRawExport: true,
|
|
1618
|
+
explanationTruncation: AUDITOR_EXPLANATION_TRUNCATION,
|
|
1619
|
+
maxWorstEvaluations: AUDITOR_MAX_WORST_EVALUATIONS,
|
|
1620
|
+
},
|
|
1621
|
+
};
|
|
1622
|
+
/**
|
|
1623
|
+
* Get the feature configuration for a role.
|
|
1624
|
+
*/
|
|
1625
|
+
export function getRoleFeatureConfig(role) {
|
|
1626
|
+
return configs[role];
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Compute sample standard deviation from an array of scores.
|
|
1630
|
+
* Returns null for arrays with fewer than 2 values.
|
|
1631
|
+
*/
|
|
1632
|
+
export function computeStdDev(values) {
|
|
1633
|
+
if (values.length < 2)
|
|
1634
|
+
return null;
|
|
1635
|
+
return roundTo(ss.sampleStandardDeviation(values), SCORE_PRECISION);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Load degradation state from persisted JSON file.
|
|
1639
|
+
* Returns empty state if file does not exist or is malformed.
|
|
1640
|
+
*/
|
|
1641
|
+
export function loadDegradationState(dir) {
|
|
1642
|
+
const filePath = join(dir, DEGRADATION_STATE_FILE);
|
|
1643
|
+
try {
|
|
1644
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
1645
|
+
const parsed = JSON.parse(raw);
|
|
1646
|
+
const result = degradationStateSchema.safeParse(parsed);
|
|
1647
|
+
if (result.success) {
|
|
1648
|
+
return result.data;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
catch { /* file not found or malformed */ }
|
|
1652
|
+
return { lastRun: '', breaches: {} };
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Save degradation state to JSON file for cross-run breach tracking.
|
|
1656
|
+
* Validates state before writing to ensure round-trip integrity.
|
|
1657
|
+
*/
|
|
1658
|
+
export function saveDegradationState(dir, state) {
|
|
1659
|
+
const validated = degradationStateSchema.parse(state);
|
|
1660
|
+
writeFileSync(join(dir, DEGRADATION_STATE_FILE), JSON.stringify(validated, null, FORMAT_JSON_INDENT_SPACES) + '\n');
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Compute degradation signals for all metrics from time-bucketed evaluation data.
|
|
1664
|
+
*
|
|
1665
|
+
* Bridges the gap between sync-to-kv's bucket structure and computeDegradationSignal()'s
|
|
1666
|
+
* input requirements:
|
|
1667
|
+
* - Computes stdDev inline from bucket scores (not available in computeAggregations)
|
|
1668
|
+
* - Defines baseline as first DEGRADATION_BASELINE_RATIO (70%) of non-empty buckets
|
|
1669
|
+
* - Uses bucket sparsity (buckets with 0 evals / total buckets) as coverage proxy
|
|
1670
|
+
* - Passes latencyP95/P50 as null (latencySkewRatio defaults to 1.0)
|
|
1671
|
+
* - Reads priorConsecutiveBreaches from persisted DegradationState
|
|
1672
|
+
*
|
|
1673
|
+
* Skips metrics with fewer than MIN_BUCKETS_FOR_SIGNAL (3) non-empty buckets.
|
|
1674
|
+
*/
|
|
1675
|
+
export function computeRollingDegradationSignals(timeBuckets, metricNames, state, window) {
|
|
1676
|
+
const reports = [];
|
|
1677
|
+
for (const metricName of metricNames) {
|
|
1678
|
+
const buckets = timeBuckets[metricName];
|
|
1679
|
+
if (!buckets || buckets.length === 0)
|
|
1680
|
+
continue;
|
|
1681
|
+
// Filter to non-empty buckets for baseline/current split
|
|
1682
|
+
const nonEmptyBuckets = buckets.filter(b => b.scores.length > 0);
|
|
1683
|
+
if (nonEmptyBuckets.length < MIN_BUCKETS_FOR_SIGNAL)
|
|
1684
|
+
continue;
|
|
1685
|
+
// Split into baseline (first 70%) and current (last bucket)
|
|
1686
|
+
const baselineEnd = Math.max(1, Math.floor(nonEmptyBuckets.length * DEGRADATION_BASELINE_RATIO));
|
|
1687
|
+
const baselineBuckets = nonEmptyBuckets.slice(0, baselineEnd);
|
|
1688
|
+
const currentBucket = nonEmptyBuckets[nonEmptyBuckets.length - 1];
|
|
1689
|
+
// Compute stdDev for baseline and current
|
|
1690
|
+
const baselineScores = baselineBuckets.flatMap(b => b.scores);
|
|
1691
|
+
const currentStdDev = computeStdDev(currentBucket.scores);
|
|
1692
|
+
const baselineStdDev = computeStdDev(baselineScores);
|
|
1693
|
+
// Bucket sparsity as coverage proxy
|
|
1694
|
+
const bucketSparsityGaps = buckets.filter(b => b.scores.length === 0).length;
|
|
1695
|
+
// Historical values: per-bucket averages, oldest-first (for EWMA).
|
|
1696
|
+
// Includes the current bucket intentionally — EWMA uses the full series to
|
|
1697
|
+
// track drift including the most recent observation. Contrast with
|
|
1698
|
+
// baselineStdDev (L2349) which uses only baselineBuckets (first 70%),
|
|
1699
|
+
// because stdDev comparison is meant to measure change vs. the stable baseline.
|
|
1700
|
+
const historicalValues = nonEmptyBuckets
|
|
1701
|
+
.map(b => b.scores.reduce((s, v) => s + v, 0) / b.scores.length);
|
|
1702
|
+
const priorBreaches = state.breaches[metricName] ?? 0;
|
|
1703
|
+
const signal = computeDegradationSignal(currentStdDev, baselineStdDev, bucketSparsityGaps, buckets.length, null, // latencyP95 unavailable
|
|
1704
|
+
null, // latencyP50 unavailable
|
|
1705
|
+
{ historicalValues, priorConsecutiveBreaches: priorBreaches });
|
|
1706
|
+
// Total evaluation count
|
|
1707
|
+
const evaluationCount = buckets.reduce((sum, b) => sum + b.scores.length, 0);
|
|
1708
|
+
// Baseline period timestamps
|
|
1709
|
+
const baselinePeriod = {
|
|
1710
|
+
startDate: baselineBuckets[0].startTime,
|
|
1711
|
+
endDate: baselineBuckets[baselineBuckets.length - 1].endTime,
|
|
1712
|
+
};
|
|
1713
|
+
reports.push({
|
|
1714
|
+
metricName,
|
|
1715
|
+
signal,
|
|
1716
|
+
trend: null, // QualityMetricConfig unavailable here; caller enriches via computeTrend()
|
|
1717
|
+
window,
|
|
1718
|
+
baselinePeriod,
|
|
1719
|
+
evaluationCount,
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return reports;
|
|
1723
|
+
}
|
|
1724
|
+
export function computeAHPWeights(comparisons, metrics) {
|
|
1725
|
+
const n = metrics.length;
|
|
1726
|
+
const equalWeight = 1 / n;
|
|
1727
|
+
if (comparisons.length === 0) {
|
|
1728
|
+
const weights = {};
|
|
1729
|
+
for (const m of metrics)
|
|
1730
|
+
weights[m] = equalWeight;
|
|
1731
|
+
return { weights, consistencyRatio: 0 };
|
|
1732
|
+
}
|
|
1733
|
+
// Build n×n comparison matrix
|
|
1734
|
+
const matrix = Array.from({ length: n }, () => Array(n).fill(1));
|
|
1735
|
+
const indexMap = {};
|
|
1736
|
+
for (let i = 0; i < n; i++) {
|
|
1737
|
+
const m = metrics[i];
|
|
1738
|
+
if (m === undefined)
|
|
1739
|
+
throw new Error(`AHP: metrics array has hole at index ${i}`);
|
|
1740
|
+
indexMap[m] = i;
|
|
1741
|
+
}
|
|
1742
|
+
for (const c of comparisons) {
|
|
1743
|
+
const i = indexMap[c.metricA];
|
|
1744
|
+
const j = indexMap[c.metricB];
|
|
1745
|
+
if (i === undefined || j === undefined)
|
|
1746
|
+
continue;
|
|
1747
|
+
const val = c.preferred === 'A' ? c.preference : 1 / c.preference;
|
|
1748
|
+
if (matrix[i])
|
|
1749
|
+
matrix[i][j] = val;
|
|
1750
|
+
if (matrix[j])
|
|
1751
|
+
matrix[j][i] = 1 / val;
|
|
1752
|
+
}
|
|
1753
|
+
// Geometric mean of each row → normalize
|
|
1754
|
+
const geoMeans = pipe(matrix, map((row) => Math.pow(row.reduce((acc, v) => acc * v, 1), 1 / n)));
|
|
1755
|
+
const geoSum = geoMeans.reduce((acc, v) => acc + v, 0);
|
|
1756
|
+
const weights = Object.fromEntries(metrics.map((m, i) => [m, (geoMeans[i] ?? 0) / geoSum]));
|
|
1757
|
+
// Consistency check
|
|
1758
|
+
const weightVec = metrics.map(m => weights[m] ?? 0);
|
|
1759
|
+
let lambdaMax = 0;
|
|
1760
|
+
for (let i = 0; i < n; i++) {
|
|
1761
|
+
let rowSum = 0;
|
|
1762
|
+
const matrixRow = matrix[i];
|
|
1763
|
+
const wi = weightVec[i];
|
|
1764
|
+
if (!matrixRow || wi === undefined || wi === 0)
|
|
1765
|
+
throw new Error(`AHP: corrupt weight vector at index ${i}`);
|
|
1766
|
+
for (let j = 0; j < n; j++)
|
|
1767
|
+
rowSum += (matrixRow[j] ?? 0) * (weightVec[j] ?? 0);
|
|
1768
|
+
lambdaMax += rowSum / wi;
|
|
1769
|
+
}
|
|
1770
|
+
lambdaMax /= n;
|
|
1771
|
+
const ci = (lambdaMax - n) / (n - 1);
|
|
1772
|
+
const ri = AHP_RANDOM_INDEX[n] ?? 0;
|
|
1773
|
+
const consistencyRatio = ri === 0 ? 0 : ci / ri;
|
|
1774
|
+
return { weights, consistencyRatio };
|
|
1775
|
+
}
|
|
1776
|
+
export function computeCRITICWeights(metricScoreHistory) {
|
|
1777
|
+
const metrics = Object.keys(metricScoreHistory);
|
|
1778
|
+
if (metrics.length === 1) {
|
|
1779
|
+
const m = metrics[0] ?? '';
|
|
1780
|
+
const sd = computeStdDev(metricScoreHistory[m] ?? []) ?? 0;
|
|
1781
|
+
return {
|
|
1782
|
+
weights: { [m]: 1.0 },
|
|
1783
|
+
informationContent: { [m]: sd },
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
// Stddev per metric
|
|
1787
|
+
const stdevs = {};
|
|
1788
|
+
for (const m of metrics)
|
|
1789
|
+
stdevs[m] = computeStdDev(metricScoreHistory[m] ?? []) ?? 0;
|
|
1790
|
+
// Pearson correlation matrix
|
|
1791
|
+
const correlations = {};
|
|
1792
|
+
for (const m of metrics) {
|
|
1793
|
+
correlations[m] = {};
|
|
1794
|
+
for (const other of metrics) {
|
|
1795
|
+
if (m === other) {
|
|
1796
|
+
if (correlations[m])
|
|
1797
|
+
correlations[m][other] = 1;
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
if (correlations[m])
|
|
1801
|
+
correlations[m][other] = computePearsonR(metricScoreHistory[m] ?? [], metricScoreHistory[other] ?? []);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Information content = stddev × (1 - mean(|correlations with other metrics|))
|
|
1806
|
+
const informationContent = {};
|
|
1807
|
+
for (const m of metrics) {
|
|
1808
|
+
const others = metrics.filter(o => o !== m);
|
|
1809
|
+
const meanAbsCorr = others.length === 0
|
|
1810
|
+
? 0
|
|
1811
|
+
: others.reduce((acc, o) => acc + Math.abs(correlations[m]?.[o] ?? 0), 0) / others.length;
|
|
1812
|
+
const ic = (stdevs[m] ?? 0) * (1 - meanAbsCorr);
|
|
1813
|
+
informationContent[m] = Number.isFinite(ic) ? ic : 0;
|
|
1814
|
+
}
|
|
1815
|
+
// Normalize to get weights
|
|
1816
|
+
const totalIC = Object.values(informationContent).reduce((a, v) => a + v, 0);
|
|
1817
|
+
const weights = {};
|
|
1818
|
+
if (totalIC === 0) {
|
|
1819
|
+
for (const m of metrics)
|
|
1820
|
+
weights[m] = 1 / metrics.length;
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
for (const m of metrics)
|
|
1824
|
+
weights[m] = (informationContent[m] ?? 0) / totalIC;
|
|
1825
|
+
}
|
|
1826
|
+
return { weights, informationContent };
|
|
1827
|
+
}
|
|
1828
|
+
export function computeHybridCQIWeights(ahpWeights, criticWeights, incidents, metrics, alpha = AHP_CRITIC_DEFAULT_ALPHA) {
|
|
1829
|
+
// Blend AHP and CRITIC weights
|
|
1830
|
+
const blended = {};
|
|
1831
|
+
for (const m of metrics) {
|
|
1832
|
+
blended[m] = alpha * (ahpWeights[m] ?? 0) + (1 - alpha) * (criticWeights[m] ?? 0);
|
|
1833
|
+
}
|
|
1834
|
+
// Normalize blended weights
|
|
1835
|
+
const blendedSum = Object.values(blended).reduce((a, v) => a + v, 0);
|
|
1836
|
+
const weights = {};
|
|
1837
|
+
for (const m of metrics) {
|
|
1838
|
+
weights[m] = blendedSum === 0 ? 1 / metrics.length : (blended[m] ?? 0) / blendedSum;
|
|
1839
|
+
}
|
|
1840
|
+
// Incident correlations
|
|
1841
|
+
const incidentCorrelations = {};
|
|
1842
|
+
if (incidents.length === 0) {
|
|
1843
|
+
for (const m of metrics)
|
|
1844
|
+
incidentCorrelations[m] = 0;
|
|
1845
|
+
}
|
|
1846
|
+
else {
|
|
1847
|
+
// For each metric: average score drop at incident times vs overall mean
|
|
1848
|
+
// Use point-biserial: incident signal = 1 at incident timestamps, 0 otherwise
|
|
1849
|
+
// Simplify: compute mean metric score at incident times; higher drop = higher correlation
|
|
1850
|
+
// Actually compute the "drop magnitude" = overallMean - incidentMean (if positive, metric drops at incidents)
|
|
1851
|
+
// Normalize across metrics to get relative correlation values
|
|
1852
|
+
const drops = {};
|
|
1853
|
+
for (const m of metrics) {
|
|
1854
|
+
const incidentScores = incidents
|
|
1855
|
+
.map(inc => inc.metricScores[m])
|
|
1856
|
+
.filter((v) => v !== undefined);
|
|
1857
|
+
const meanAtIncident = incidentScores.length === 0
|
|
1858
|
+
? 0
|
|
1859
|
+
: incidentScores.reduce((a, v) => a + v, 0) / incidentScores.length;
|
|
1860
|
+
drops[m] = 1 - meanAtIncident; // higher drop value means more correlated with incident
|
|
1861
|
+
}
|
|
1862
|
+
// Normalize drops to sum to 1 if there's signal, else keep raw
|
|
1863
|
+
const maxDrop = Math.max(...Object.values(drops));
|
|
1864
|
+
const minDrop = Math.min(...Object.values(drops));
|
|
1865
|
+
const range = maxDrop - minDrop;
|
|
1866
|
+
for (const m of metrics) {
|
|
1867
|
+
incidentCorrelations[m] = range === 0 ? 0 : ((drops[m] ?? 0) - minDrop) / range;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
featureVersion: TUNED_CQI_WEIGHTS_FEATURE_VERSION,
|
|
1872
|
+
weights,
|
|
1873
|
+
ahpWeights,
|
|
1874
|
+
criticWeights,
|
|
1875
|
+
alpha,
|
|
1876
|
+
consistencyRatio: 0,
|
|
1877
|
+
incidentCorrelations,
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
const ZERO_TAPR_METRICS = { precision: 0, recall: 0, f1: 0, detectionDelay: 0 };
|
|
1881
|
+
export function computeTaPR(detectedRanges, incidentRanges) {
|
|
1882
|
+
if (detectedRanges.length === 0 || incidentRanges.length === 0)
|
|
1883
|
+
return ZERO_TAPR_METRICS;
|
|
1884
|
+
const overlapMs = (a, b) => Math.max(0, Math.min(a.end, b.end) - Math.max(a.start, b.start));
|
|
1885
|
+
// Precision: average overlap ratio per detected range
|
|
1886
|
+
let precisionSum = 0;
|
|
1887
|
+
for (const d of detectedRanges) {
|
|
1888
|
+
const dDuration = d.end - d.start;
|
|
1889
|
+
if (dDuration <= 0)
|
|
1890
|
+
continue;
|
|
1891
|
+
let overlap = 0;
|
|
1892
|
+
for (const inc of incidentRanges)
|
|
1893
|
+
overlap += overlapMs(d, inc);
|
|
1894
|
+
precisionSum += Math.min(1, overlap / dDuration);
|
|
1895
|
+
}
|
|
1896
|
+
const precision = precisionSum / detectedRanges.length;
|
|
1897
|
+
// Recall: average overlap ratio per incident range
|
|
1898
|
+
let recallSum = 0;
|
|
1899
|
+
for (const inc of incidentRanges) {
|
|
1900
|
+
const incDuration = inc.end - inc.start;
|
|
1901
|
+
if (incDuration <= 0)
|
|
1902
|
+
continue;
|
|
1903
|
+
let overlap = 0;
|
|
1904
|
+
for (const d of detectedRanges)
|
|
1905
|
+
overlap += overlapMs(d, inc);
|
|
1906
|
+
recallSum += Math.min(1, overlap / incDuration);
|
|
1907
|
+
}
|
|
1908
|
+
const recall = recallSum / incidentRanges.length;
|
|
1909
|
+
const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
|
|
1910
|
+
// Detection delay: for each incident with an overlapping detection, delay = max(0, firstOverlappingDetection.start - incident.start)
|
|
1911
|
+
const delays = [];
|
|
1912
|
+
for (const inc of incidentRanges) {
|
|
1913
|
+
const overlapping = detectedRanges.filter(d => overlapMs(d, inc) > 0);
|
|
1914
|
+
if (overlapping.length === 0)
|
|
1915
|
+
continue;
|
|
1916
|
+
const firstStart = Math.min(...overlapping.map(d => d.start));
|
|
1917
|
+
delays.push(Math.max(0, firstStart - inc.start));
|
|
1918
|
+
}
|
|
1919
|
+
const detectionDelay = delays.length > 0 ? delays.reduce((a, b) => a + b, 0) / delays.length : 0;
|
|
1920
|
+
return { precision, recall, f1, detectionDelay };
|
|
1921
|
+
}
|
|
1922
|
+
/** Returns true if any signal for the point exceeds its configured threshold. */
|
|
1923
|
+
function isPointBreaching(pt, config) {
|
|
1924
|
+
const varianceRatio = pt.baselineStdDev > 0 ? pt.currentStdDev / pt.baselineStdDev : 0;
|
|
1925
|
+
const dropoutRate = pt.totalCoverageCells > 0 ? pt.coverageGapCount / pt.totalCoverageCells : 0;
|
|
1926
|
+
const skewRatio = pt.latencyP50 > 0 ? pt.latencyP95 / pt.latencyP50 : 0;
|
|
1927
|
+
const ewmaDrift = config.ewmaLambda !== undefined && pt.historicalValues.length > 0
|
|
1928
|
+
&& detectEWMADrift(pt.historicalValues, config.ewmaLambda);
|
|
1929
|
+
const signals = [
|
|
1930
|
+
varianceRatio > config.varianceThreshold,
|
|
1931
|
+
dropoutRate > config.coverageDropoutThreshold,
|
|
1932
|
+
skewRatio > config.latencySkewThreshold,
|
|
1933
|
+
ewmaDrift,
|
|
1934
|
+
];
|
|
1935
|
+
return signals.some(Boolean);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Runs a degradation signal backtest with a specific parameter configuration.
|
|
1939
|
+
* Replays historical time series data and compares detected degradation periods
|
|
1940
|
+
* against labeled incidents using TaPR and point-based metrics.
|
|
1941
|
+
*/
|
|
1942
|
+
export function backtestDegradationConfig(config, timeSeries, incidents) {
|
|
1943
|
+
if (timeSeries.length === 0) {
|
|
1944
|
+
return { config, truePositives: 0, falsePositives: 0, falseNegatives: 0, trueNegatives: 0,
|
|
1945
|
+
tapr: ZERO_TAPR_METRICS, pointPrecision: 0, pointRecall: 0, pointF1: 0 };
|
|
1946
|
+
}
|
|
1947
|
+
const incidentRanges = incidents.map(inc => ({
|
|
1948
|
+
start: new Date(inc.startTime).getTime(),
|
|
1949
|
+
end: new Date(inc.endTime).getTime(),
|
|
1950
|
+
}));
|
|
1951
|
+
// Classify each timestamp as breaching or not
|
|
1952
|
+
const breaching = timeSeries.map(pt => isPointBreaching(pt, config));
|
|
1953
|
+
// Build confirmed degradation state per timestamp (confirmationWindow consecutive breaches)
|
|
1954
|
+
const confirmed = new Array(timeSeries.length).fill(false);
|
|
1955
|
+
let consecutiveBreaches = 0;
|
|
1956
|
+
for (let i = 0; i < timeSeries.length; i++) {
|
|
1957
|
+
consecutiveBreaches = breaching[i] ? consecutiveBreaches + 1 : 0;
|
|
1958
|
+
confirmed[i] = consecutiveBreaches >= config.confirmationWindow;
|
|
1959
|
+
}
|
|
1960
|
+
// Build detected ranges from consecutive confirmed timestamps
|
|
1961
|
+
const detectedRanges = [];
|
|
1962
|
+
let rangeStart = null;
|
|
1963
|
+
for (let i = 0; i < timeSeries.length; i++) {
|
|
1964
|
+
if (confirmed[i]) {
|
|
1965
|
+
if (rangeStart === null)
|
|
1966
|
+
rangeStart = timeSeries[i].timestamp;
|
|
1967
|
+
}
|
|
1968
|
+
else {
|
|
1969
|
+
if (rangeStart !== null) {
|
|
1970
|
+
detectedRanges.push({ start: rangeStart, end: timeSeries[i - 1].timestamp });
|
|
1971
|
+
rangeStart = null;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (rangeStart !== null) {
|
|
1976
|
+
detectedRanges.push({ start: rangeStart, end: timeSeries[timeSeries.length - 1].timestamp });
|
|
1977
|
+
}
|
|
1978
|
+
const tapr = computeTaPR(detectedRanges, incidentRanges);
|
|
1979
|
+
// Point-based classification
|
|
1980
|
+
let tp = 0, fp = 0, fn = 0, tn = 0;
|
|
1981
|
+
for (let i = 0; i < timeSeries.length; i++) {
|
|
1982
|
+
const ts = timeSeries[i].timestamp;
|
|
1983
|
+
const inIncident = incidentRanges.some(r => ts >= r.start && ts <= r.end);
|
|
1984
|
+
if (confirmed[i]) {
|
|
1985
|
+
if (inIncident)
|
|
1986
|
+
tp++;
|
|
1987
|
+
else
|
|
1988
|
+
fp++;
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
if (inIncident)
|
|
1992
|
+
fn++;
|
|
1993
|
+
else
|
|
1994
|
+
tn++;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const pointPrecision = tp + fp > 0 ? tp / (tp + fp) : 0;
|
|
1998
|
+
const pointRecall = tp + fn > 0 ? tp / (tp + fn) : 0;
|
|
1999
|
+
const pointF1 = pointPrecision + pointRecall > 0
|
|
2000
|
+
? (2 * pointPrecision * pointRecall) / (pointPrecision + pointRecall)
|
|
2001
|
+
: 0;
|
|
2002
|
+
return { config, truePositives: tp, falsePositives: fp, falseNegatives: fn, trueNegatives: tn,
|
|
2003
|
+
tapr, pointPrecision, pointRecall, pointF1 };
|
|
2004
|
+
}
|
|
2005
|
+
/** All cartesian-product BacktestConfig combinations from BACKTEST_SWEEP */
|
|
2006
|
+
const SWEEP_CONFIGS = (() => {
|
|
2007
|
+
const { varianceThreshold, coverageDropoutThreshold, ewmaLambda, confirmationWindow, stabilityThreshold } = BACKTEST_SWEEP;
|
|
2008
|
+
const configs = [];
|
|
2009
|
+
for (const vt of varianceThreshold) {
|
|
2010
|
+
for (const cdt of coverageDropoutThreshold) {
|
|
2011
|
+
for (const lambda of ewmaLambda) {
|
|
2012
|
+
for (const cw of confirmationWindow) {
|
|
2013
|
+
for (const st of stabilityThreshold) {
|
|
2014
|
+
configs.push({ varianceThreshold: vt, coverageDropoutThreshold: cdt, ewmaLambda: lambda, confirmationWindow: cw, stabilityThreshold: st, latencySkewThreshold: LATENCY_SKEW_SIGNAL_THRESHOLD });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return configs;
|
|
2021
|
+
})();
|
|
2022
|
+
/**
|
|
2023
|
+
* Runs a full parameter sweep across the BACKTEST_SWEEP grid, ranks results by TaPR F1,
|
|
2024
|
+
* and includes the current production config as a baseline comparison.
|
|
2025
|
+
*/
|
|
2026
|
+
export function sweepDegradationParams(timeSeries, incidents) {
|
|
2027
|
+
const configs = SWEEP_CONFIGS;
|
|
2028
|
+
const results = configs.map(config => backtestDegradationConfig(config, timeSeries, incidents));
|
|
2029
|
+
const currentConfigResult = backtestDegradationConfig(CURRENT_PRODUCTION_CONFIG, timeSeries, incidents);
|
|
2030
|
+
const bestByF1 = results.reduce((best, r) => r.tapr.f1 > best.tapr.f1 ? r : best, results[0]);
|
|
2031
|
+
const bestByRecall = results.reduce((best, r) => r.tapr.recall > best.tapr.recall ? r : best, results[0]);
|
|
2032
|
+
return { results, bestByF1, bestByRecall, currentConfigResult };
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Leave-one-out cross-validation for sweepDegradationParams.
|
|
2036
|
+
*
|
|
2037
|
+
* With only 5-10 incidents, overfitting risk is high. This function holds out
|
|
2038
|
+
* each incident in turn, runs the full sweep on the remaining incidents, and
|
|
2039
|
+
* reports variance in the optimal config selection across folds.
|
|
2040
|
+
*
|
|
2041
|
+
* High parameterVariance values indicate that the best config is sensitive to
|
|
2042
|
+
* individual incidents and should not be trusted for threshold graduation.
|
|
2043
|
+
*/
|
|
2044
|
+
export function sweepWithCrossValidation(timeSeries, incidents) {
|
|
2045
|
+
const fullSweep = sweepDegradationParams(timeSeries, incidents);
|
|
2046
|
+
// LOO folds — each fold holds out one incident
|
|
2047
|
+
const folds = incidents.map((heldOut, i) => {
|
|
2048
|
+
const remaining = incidents.filter((_, j) => j !== i);
|
|
2049
|
+
return { heldOutIncident: heldOut, sweepResult: sweepDegradationParams(timeSeries, remaining) };
|
|
2050
|
+
});
|
|
2051
|
+
// Compute std dev of best-by-F1 config parameters across folds
|
|
2052
|
+
const paramValues = folds.map(f => f.sweepResult.bestByF1.config);
|
|
2053
|
+
const varStd = computeStdDev(paramValues.map(c => c.varianceThreshold)) ?? 0;
|
|
2054
|
+
const dropStd = computeStdDev(paramValues.map(c => c.coverageDropoutThreshold)) ?? 0;
|
|
2055
|
+
const winStd = computeStdDev(paramValues.map(c => c.confirmationWindow)) ?? 0;
|
|
2056
|
+
// Stable when every LOO fold selects the same best-by-F1 config as the full sweep.
|
|
2057
|
+
// Values are from a discrete sweep grid, so exact equality is safe.
|
|
2058
|
+
const fullBest = fullSweep.bestByF1.config;
|
|
2059
|
+
const isStable = folds.every(f => {
|
|
2060
|
+
const fb = f.sweepResult.bestByF1.config;
|
|
2061
|
+
return fb.varianceThreshold === fullBest.varianceThreshold &&
|
|
2062
|
+
fb.coverageDropoutThreshold === fullBest.coverageDropoutThreshold &&
|
|
2063
|
+
fb.confirmationWindow === fullBest.confirmationWindow;
|
|
2064
|
+
});
|
|
2065
|
+
return {
|
|
2066
|
+
fullSweep,
|
|
2067
|
+
folds,
|
|
2068
|
+
parameterStability: {
|
|
2069
|
+
varianceThresholdStdDev: varStd,
|
|
2070
|
+
coverageDropoutThresholdStdDev: dropStd,
|
|
2071
|
+
confirmationWindowStdDev: winStd,
|
|
2072
|
+
},
|
|
2073
|
+
isStable,
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
//# sourceMappingURL=quality-feature-engineering.js.map
|