nat-engine 1__py3-none-any.whl
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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Belief-Driven Test Prioritizer.
|
|
7
|
+
|
|
8
|
+
Uses BDI agent belief states to rank test cases by predicted fault likelihood,
|
|
9
|
+
concentrating testing effort on the highest-risk endpoints first.
|
|
10
|
+
|
|
11
|
+
Supported strategies
|
|
12
|
+
--------------------
|
|
13
|
+
- ``risk_first`` — Sort by descending risk score (default).
|
|
14
|
+
- ``round_robin`` — Cycle evenly across all endpoints.
|
|
15
|
+
- ``exploration`` — ε-greedy: exploit high-risk endpoints most of the time,
|
|
16
|
+
but occasionally explore lower-risk ones to avoid blind
|
|
17
|
+
spots (mirrors the BDI exploration quota from §4.5).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import random
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from mannf.core.agents.belief_state import BeliefState
|
|
29
|
+
from mannf.core.testing.models import TestCase, TestResult
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PrioritizationStrategy(str, Enum):
|
|
35
|
+
"""Available test prioritization strategies."""
|
|
36
|
+
|
|
37
|
+
RISK_FIRST = "risk_first"
|
|
38
|
+
ROUND_ROBIN = "round_robin"
|
|
39
|
+
EXPLORATION = "exploration"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BeliefPrioritizer:
|
|
43
|
+
"""Ranks and prioritizes test cases using BDI agent belief states.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
strategy:
|
|
48
|
+
Which prioritization strategy to apply.
|
|
49
|
+
exploration_epsilon:
|
|
50
|
+
For the ``exploration`` strategy — probability of picking a
|
|
51
|
+
non-highest-risk endpoint (0 = fully exploit, 1 = fully random).
|
|
52
|
+
epsilon_decay:
|
|
53
|
+
Multiplicative decay applied to *exploration_epsilon* after each
|
|
54
|
+
:meth:`record_result` call.
|
|
55
|
+
min_epsilon:
|
|
56
|
+
Floor for *exploration_epsilon* after decay.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
strategy: PrioritizationStrategy | str = PrioritizationStrategy.RISK_FIRST,
|
|
62
|
+
exploration_epsilon: float = 0.2,
|
|
63
|
+
epsilon_decay: float = 0.99,
|
|
64
|
+
min_epsilon: float = 0.05,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.strategy = PrioritizationStrategy(strategy)
|
|
67
|
+
self.epsilon = exploration_epsilon
|
|
68
|
+
self.epsilon_decay = epsilon_decay
|
|
69
|
+
self.min_epsilon = min_epsilon
|
|
70
|
+
|
|
71
|
+
# Historical fault tracking per endpoint target
|
|
72
|
+
self._fault_counts: Dict[str, int] = defaultdict(int)
|
|
73
|
+
self._run_counts: Dict[str, int] = defaultdict(int)
|
|
74
|
+
|
|
75
|
+
# Round-robin index per endpoint (for round_robin strategy)
|
|
76
|
+
self._rr_index: int = 0
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Core prioritization
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def prioritize(
|
|
83
|
+
self,
|
|
84
|
+
test_cases: List[TestCase],
|
|
85
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
86
|
+
) -> List[TestCase]:
|
|
87
|
+
"""Return *test_cases* re-ordered by the configured strategy.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
test_cases:
|
|
92
|
+
Unordered list of test cases to prioritize.
|
|
93
|
+
belief_states:
|
|
94
|
+
Current BDI agent belief states used to derive per-endpoint risk
|
|
95
|
+
scores. If *None*, only historical fault rates are used.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
List[TestCase]
|
|
100
|
+
A new list with the same elements, sorted highest-risk first
|
|
101
|
+
(or in the strategy-appropriate order).
|
|
102
|
+
"""
|
|
103
|
+
if not test_cases:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
risk_scores = self._compute_risk_scores(test_cases, belief_states)
|
|
107
|
+
|
|
108
|
+
if self.strategy == PrioritizationStrategy.RISK_FIRST:
|
|
109
|
+
return self._sort_risk_first(test_cases, risk_scores)
|
|
110
|
+
if self.strategy == PrioritizationStrategy.ROUND_ROBIN:
|
|
111
|
+
return self._sort_round_robin(test_cases)
|
|
112
|
+
if self.strategy == PrioritizationStrategy.EXPLORATION:
|
|
113
|
+
return self._sort_exploration(test_cases, risk_scores)
|
|
114
|
+
|
|
115
|
+
# Fallback
|
|
116
|
+
return test_cases[:]
|
|
117
|
+
|
|
118
|
+
def get_risk_scores(
|
|
119
|
+
self,
|
|
120
|
+
test_cases: List[TestCase],
|
|
121
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
122
|
+
) -> Dict[str, float]:
|
|
123
|
+
"""Return per-endpoint risk scores (0.0–1.0) for the given test cases."""
|
|
124
|
+
return self._compute_risk_scores(test_cases, belief_states)
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# Learning
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def record_result(self, result: TestResult, target: str) -> None:
|
|
131
|
+
"""Update historical fault rates with the observed test result.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
result:
|
|
136
|
+
Outcome of the executed test.
|
|
137
|
+
target:
|
|
138
|
+
Endpoint target string (e.g. ``"/users"``).
|
|
139
|
+
"""
|
|
140
|
+
self._run_counts[target] += 1
|
|
141
|
+
if not result.passed or result.error:
|
|
142
|
+
self._fault_counts[target] += 1
|
|
143
|
+
|
|
144
|
+
# Decay exploration rate
|
|
145
|
+
self.epsilon = max(self.min_epsilon, self.epsilon * self.epsilon_decay)
|
|
146
|
+
|
|
147
|
+
def historical_fault_rate(self, target: str) -> float:
|
|
148
|
+
"""Return the observed fault rate for *target* (0.0–1.0)."""
|
|
149
|
+
runs = self._run_counts.get(target, 0)
|
|
150
|
+
if runs == 0:
|
|
151
|
+
return 0.5 # Uninformed prior
|
|
152
|
+
return self._fault_counts.get(target, 0) / runs
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
# Internal helpers
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def _compute_risk_scores(
|
|
159
|
+
self,
|
|
160
|
+
test_cases: List[TestCase],
|
|
161
|
+
belief_states: Optional[List[BeliefState]],
|
|
162
|
+
) -> Dict[str, float]:
|
|
163
|
+
"""Compute a composite risk score per unique endpoint target."""
|
|
164
|
+
targets = {tc.target for tc in test_cases}
|
|
165
|
+
scores: Dict[str, float] = {}
|
|
166
|
+
|
|
167
|
+
for target in targets:
|
|
168
|
+
# Component 1: historical fault rate
|
|
169
|
+
hist_rate = self.historical_fault_rate(target)
|
|
170
|
+
|
|
171
|
+
# Component 2: BDI belief state signal (average across agents)
|
|
172
|
+
belief_signal = 0.5 # uninformed prior
|
|
173
|
+
if belief_states:
|
|
174
|
+
signals = []
|
|
175
|
+
for bs in belief_states:
|
|
176
|
+
val = bs.fault_likelihood.get(target)
|
|
177
|
+
if val is not None:
|
|
178
|
+
signals.append(val)
|
|
179
|
+
if signals:
|
|
180
|
+
belief_signal = sum(signals) / len(signals)
|
|
181
|
+
|
|
182
|
+
# Component 3: test case priority field (if set by upstream)
|
|
183
|
+
tc_priorities = [
|
|
184
|
+
tc.priority for tc in test_cases if tc.target == target
|
|
185
|
+
]
|
|
186
|
+
tc_priority = (
|
|
187
|
+
sum(tc_priorities) / len(tc_priorities) if tc_priorities else 0.5
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Weighted composite (belief state has highest weight)
|
|
191
|
+
score = 0.4 * belief_signal + 0.4 * hist_rate + 0.2 * tc_priority
|
|
192
|
+
scores[target] = min(1.0, max(0.0, score))
|
|
193
|
+
|
|
194
|
+
return scores
|
|
195
|
+
|
|
196
|
+
def _sort_risk_first(
|
|
197
|
+
self, test_cases: List[TestCase], risk_scores: Dict[str, float]
|
|
198
|
+
) -> List[TestCase]:
|
|
199
|
+
return sorted(
|
|
200
|
+
test_cases,
|
|
201
|
+
key=lambda tc: risk_scores.get(tc.target, 0.5),
|
|
202
|
+
reverse=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _sort_round_robin(self, test_cases: List[TestCase]) -> List[TestCase]:
|
|
206
|
+
"""Interleave test cases across endpoints in a round-robin fashion."""
|
|
207
|
+
by_target: Dict[str, List[TestCase]] = defaultdict(list)
|
|
208
|
+
for tc in test_cases:
|
|
209
|
+
by_target[tc.target].append(tc)
|
|
210
|
+
|
|
211
|
+
ordered_targets = sorted(by_target.keys())
|
|
212
|
+
result: List[TestCase] = []
|
|
213
|
+
while any(by_target[t] for t in ordered_targets):
|
|
214
|
+
for t in ordered_targets:
|
|
215
|
+
if by_target[t]:
|
|
216
|
+
result.append(by_target[t].pop(0))
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
def _sort_exploration(
|
|
220
|
+
self, test_cases: List[TestCase], risk_scores: Dict[str, float]
|
|
221
|
+
) -> List[TestCase]:
|
|
222
|
+
"""ε-greedy: exploit high-risk ordering most of the time."""
|
|
223
|
+
if random.random() < self.epsilon:
|
|
224
|
+
# Explore: random shuffle
|
|
225
|
+
shuffled = test_cases[:]
|
|
226
|
+
random.shuffle(shuffled)
|
|
227
|
+
logger.debug("BeliefPrioritizer: exploration — shuffled %d tests", len(shuffled))
|
|
228
|
+
return shuffled
|
|
229
|
+
# Exploit: risk-first
|
|
230
|
+
logger.debug("BeliefPrioritizer: exploitation — risk-first ordering")
|
|
231
|
+
return self._sort_risk_first(test_cases, risk_scores)
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Endpoint Risk Scoring Engine.
|
|
7
|
+
|
|
8
|
+
Computes and maintains per-endpoint composite risk scores by combining:
|
|
9
|
+
|
|
10
|
+
* **Historical fault rate** — percentage of past test failures per endpoint.
|
|
11
|
+
* **Belief state confidence** — BDI agent confidence in fault likelihood.
|
|
12
|
+
* **Response time anomalies** — endpoints with increasing latency trends.
|
|
13
|
+
* **Error pattern diversity** — endpoints showing varied error types.
|
|
14
|
+
* **Connection-oriented metrics** — disconnect rate and message latency for
|
|
15
|
+
WebSocket and gRPC streaming endpoints.
|
|
16
|
+
|
|
17
|
+
The :meth:`get_risk_report` method returns a structured, JSON-serializable
|
|
18
|
+
report suitable for the REST API and future dashboard.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import math
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
from mannf.core.agents.belief_state import BeliefState
|
|
29
|
+
from mannf.core.testing.models import TestResult
|
|
30
|
+
|
|
31
|
+
# Sliding window sizes for history tracking
|
|
32
|
+
_LATENCY_WINDOW = 50
|
|
33
|
+
_MSG_LATENCY_WINDOW = 50
|
|
34
|
+
_SCORE_HISTORY_WINDOW = 10
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TrendDirection(str):
|
|
38
|
+
"""Trend direction constants."""
|
|
39
|
+
|
|
40
|
+
IMPROVING = "improving"
|
|
41
|
+
DEGRADING = "degrading"
|
|
42
|
+
STABLE = "stable"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RiskScorer:
|
|
46
|
+
"""Computes and tracks per-endpoint composite risk scores.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
belief_weight:
|
|
51
|
+
Weight for the BDI belief state signal (0–1).
|
|
52
|
+
fault_rate_weight:
|
|
53
|
+
Weight for the historical fault rate signal (0–1).
|
|
54
|
+
latency_weight:
|
|
55
|
+
Weight for the response-time anomaly signal (0–1).
|
|
56
|
+
diversity_weight:
|
|
57
|
+
Weight for the error pattern diversity signal (0–1).
|
|
58
|
+
connection_weight:
|
|
59
|
+
Weight for the connection-oriented signal (WebSocket disconnect rate,
|
|
60
|
+
gRPC stream errors) (0–1). Defaults to 0.10.
|
|
61
|
+
|
|
62
|
+
The five weights should sum to 1.0 for a normalized composite score;
|
|
63
|
+
they are automatically re-normalized if they don't.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
belief_weight: float = 0.30,
|
|
69
|
+
fault_rate_weight: float = 0.30,
|
|
70
|
+
latency_weight: float = 0.20,
|
|
71
|
+
diversity_weight: float = 0.10,
|
|
72
|
+
connection_weight: float = 0.10,
|
|
73
|
+
) -> None:
|
|
74
|
+
total = belief_weight + fault_rate_weight + latency_weight + diversity_weight + connection_weight
|
|
75
|
+
if total <= 0:
|
|
76
|
+
total = 1.0
|
|
77
|
+
self._w_belief = belief_weight / total
|
|
78
|
+
self._w_fault = fault_rate_weight / total
|
|
79
|
+
self._w_latency = latency_weight / total
|
|
80
|
+
self._w_diversity = diversity_weight / total
|
|
81
|
+
self._w_connection = connection_weight / total
|
|
82
|
+
|
|
83
|
+
# Per-endpoint counters
|
|
84
|
+
self._run_counts: Dict[str, int] = defaultdict(int)
|
|
85
|
+
self._fault_counts: Dict[str, int] = defaultdict(int)
|
|
86
|
+
|
|
87
|
+
# Response time history (sliding window of last 50 samples)
|
|
88
|
+
self._latency_history: Dict[str, List[float]] = defaultdict(list)
|
|
89
|
+
self._latency_window = _LATENCY_WINDOW
|
|
90
|
+
|
|
91
|
+
# Error type sets per endpoint (tracks diversity)
|
|
92
|
+
self._error_types: Dict[str, set] = defaultdict(set)
|
|
93
|
+
|
|
94
|
+
# Snapshot of the last computed risk scores
|
|
95
|
+
self._last_scores: Dict[str, float] = {}
|
|
96
|
+
|
|
97
|
+
# Score history for trend detection (last 10 snapshots per endpoint)
|
|
98
|
+
self._score_history: Dict[str, List[float]] = defaultdict(list)
|
|
99
|
+
|
|
100
|
+
# Connection-oriented metrics (WebSocket / gRPC streaming)
|
|
101
|
+
self._disconnect_counts: Dict[str, int] = defaultdict(int)
|
|
102
|
+
self._message_latency_history: Dict[str, List[float]] = defaultdict(list)
|
|
103
|
+
self._msg_latency_window = _MSG_LATENCY_WINDOW
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Ingestion
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def record(
|
|
110
|
+
self,
|
|
111
|
+
endpoint: str,
|
|
112
|
+
result: TestResult,
|
|
113
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
114
|
+
) -> float:
|
|
115
|
+
"""Record a test result and return the updated risk score for *endpoint*.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
endpoint:
|
|
120
|
+
Target endpoint identifier (e.g. ``"/users GET"``).
|
|
121
|
+
result:
|
|
122
|
+
Test execution outcome.
|
|
123
|
+
belief_states:
|
|
124
|
+
Optional BDI belief states from the current agents.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
float
|
|
129
|
+
Updated composite risk score in [0.0, 1.0].
|
|
130
|
+
"""
|
|
131
|
+
self._run_counts[endpoint] += 1
|
|
132
|
+
|
|
133
|
+
# Track faults
|
|
134
|
+
if not result.passed or result.error:
|
|
135
|
+
self._fault_counts[endpoint] += 1
|
|
136
|
+
if result.error:
|
|
137
|
+
# Capture error type from message prefix (first word)
|
|
138
|
+
err_type = result.error.split()[0] if result.error else "unknown"
|
|
139
|
+
self._error_types[endpoint].add(err_type)
|
|
140
|
+
|
|
141
|
+
# Track latency
|
|
142
|
+
latency_ms = result.execution_time_ms
|
|
143
|
+
history = self._latency_history[endpoint]
|
|
144
|
+
history.append(latency_ms)
|
|
145
|
+
if len(history) > self._latency_window:
|
|
146
|
+
history.pop(0)
|
|
147
|
+
|
|
148
|
+
score = self._compute(endpoint, belief_states)
|
|
149
|
+
self._last_scores[endpoint] = score
|
|
150
|
+
|
|
151
|
+
hist = self._score_history[endpoint]
|
|
152
|
+
hist.append(score)
|
|
153
|
+
if len(hist) > 10:
|
|
154
|
+
hist.pop(0)
|
|
155
|
+
|
|
156
|
+
return score
|
|
157
|
+
|
|
158
|
+
def record_disconnect(self, endpoint: str) -> None:
|
|
159
|
+
"""Record an unexpected connection disconnect for *endpoint*.
|
|
160
|
+
|
|
161
|
+
Should be called by :class:`~mannf.product.integrations.websocket_sut.WebSocketSUT`
|
|
162
|
+
and :class:`~mannf.product.integrations.grpc_sut.GrpcSUT` when a
|
|
163
|
+
connection drops unexpectedly.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
endpoint:
|
|
168
|
+
The WebSocket channel path or gRPC method path.
|
|
169
|
+
"""
|
|
170
|
+
self._disconnect_counts[endpoint] += 1
|
|
171
|
+
|
|
172
|
+
def record_message_latency(self, endpoint: str, latency_ms: float) -> None:
|
|
173
|
+
"""Record a send→receive message latency sample for *endpoint*.
|
|
174
|
+
|
|
175
|
+
Used to track WebSocket and gRPC streaming message round-trip times
|
|
176
|
+
independently from HTTP request latencies.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
endpoint:
|
|
181
|
+
The WebSocket channel path or gRPC method path.
|
|
182
|
+
latency_ms:
|
|
183
|
+
Observed latency in milliseconds.
|
|
184
|
+
"""
|
|
185
|
+
hist = self._message_latency_history[endpoint]
|
|
186
|
+
hist.append(latency_ms)
|
|
187
|
+
if len(hist) > self._msg_latency_window:
|
|
188
|
+
hist.pop(0)
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
# Scoring
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def score(
|
|
195
|
+
self,
|
|
196
|
+
endpoint: str,
|
|
197
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
198
|
+
) -> float:
|
|
199
|
+
"""Return the current composite risk score for *endpoint* (0.0–1.0)."""
|
|
200
|
+
return self._compute(endpoint, belief_states)
|
|
201
|
+
|
|
202
|
+
def _compute(
|
|
203
|
+
self,
|
|
204
|
+
endpoint: str,
|
|
205
|
+
belief_states: Optional[List[BeliefState]],
|
|
206
|
+
) -> float:
|
|
207
|
+
fault_rate = self._fault_rate(endpoint)
|
|
208
|
+
belief_signal = self._belief_signal(endpoint, belief_states)
|
|
209
|
+
latency_signal = self._latency_signal(endpoint)
|
|
210
|
+
diversity_signal = self._diversity_signal(endpoint)
|
|
211
|
+
connection_signal = self._connection_signal(endpoint)
|
|
212
|
+
|
|
213
|
+
score = (
|
|
214
|
+
self._w_belief * belief_signal
|
|
215
|
+
+ self._w_fault * fault_rate
|
|
216
|
+
+ self._w_latency * latency_signal
|
|
217
|
+
+ self._w_diversity * diversity_signal
|
|
218
|
+
+ self._w_connection * connection_signal
|
|
219
|
+
)
|
|
220
|
+
return min(1.0, max(0.0, score))
|
|
221
|
+
|
|
222
|
+
def _fault_rate(self, endpoint: str) -> float:
|
|
223
|
+
runs = self._run_counts.get(endpoint, 0)
|
|
224
|
+
if runs == 0:
|
|
225
|
+
return 0.5 # uninformed prior
|
|
226
|
+
return self._fault_counts.get(endpoint, 0) / runs
|
|
227
|
+
|
|
228
|
+
def _belief_signal(
|
|
229
|
+
self,
|
|
230
|
+
endpoint: str,
|
|
231
|
+
belief_states: Optional[List[BeliefState]],
|
|
232
|
+
) -> float:
|
|
233
|
+
if not belief_states:
|
|
234
|
+
return self._fault_rate(endpoint) # fall back to fault rate
|
|
235
|
+
signals = [
|
|
236
|
+
bs.fault_likelihood.get(endpoint, 0.5)
|
|
237
|
+
for bs in belief_states
|
|
238
|
+
]
|
|
239
|
+
return sum(signals) / len(signals)
|
|
240
|
+
|
|
241
|
+
def _latency_signal(self, endpoint: str) -> float:
|
|
242
|
+
"""Normalised latency anomaly score: 0 = stable, 1 = rapidly growing."""
|
|
243
|
+
history = self._latency_history.get(endpoint, [])
|
|
244
|
+
if len(history) < 4:
|
|
245
|
+
return 0.0
|
|
246
|
+
# Split into two halves and compare means
|
|
247
|
+
mid = len(history) // 2
|
|
248
|
+
first_half = history[:mid]
|
|
249
|
+
second_half = history[mid:]
|
|
250
|
+
mean_first = sum(first_half) / len(first_half)
|
|
251
|
+
mean_second = sum(second_half) / len(second_half)
|
|
252
|
+
if mean_first <= 0:
|
|
253
|
+
return 0.0
|
|
254
|
+
growth_ratio = (mean_second - mean_first) / mean_first
|
|
255
|
+
# Normalize: 50% growth → score of 0.5; cap at 1.0
|
|
256
|
+
return min(1.0, max(0.0, growth_ratio))
|
|
257
|
+
|
|
258
|
+
def _diversity_signal(self, endpoint: str) -> float:
|
|
259
|
+
"""Error type diversity → 0 (no errors) to 1.0 (many distinct types)."""
|
|
260
|
+
n_types = len(self._error_types.get(endpoint, set()))
|
|
261
|
+
if n_types == 0:
|
|
262
|
+
return 0.0
|
|
263
|
+
# log scale: 1 type → ~0.3, 3 types → ~0.8, 7+ types → 1.0
|
|
264
|
+
return min(1.0, math.log(n_types + 1) / math.log(8))
|
|
265
|
+
|
|
266
|
+
def _connection_signal(self, endpoint: str) -> float:
|
|
267
|
+
"""Connection-oriented risk signal (0 = healthy, 1 = high disconnect/latency).
|
|
268
|
+
|
|
269
|
+
Combines:
|
|
270
|
+
* **Disconnect rate** — fraction of ``record_disconnect`` calls relative
|
|
271
|
+
to total runs. A service that drops 50% of connections scores 0.5
|
|
272
|
+
on this dimension.
|
|
273
|
+
* **Message latency growth** — like ``_latency_signal`` but applied to
|
|
274
|
+
the message-latency samples recorded via
|
|
275
|
+
:meth:`record_message_latency`.
|
|
276
|
+
"""
|
|
277
|
+
runs = self._run_counts.get(endpoint, 0)
|
|
278
|
+
disconnect_count = self._disconnect_counts.get(endpoint, 0)
|
|
279
|
+
disconnect_rate = disconnect_count / max(runs, 1)
|
|
280
|
+
|
|
281
|
+
# Message latency growth signal
|
|
282
|
+
msg_history = self._message_latency_history.get(endpoint, [])
|
|
283
|
+
msg_latency_signal = 0.0
|
|
284
|
+
if len(msg_history) >= 4:
|
|
285
|
+
mid = len(msg_history) // 2
|
|
286
|
+
first_half = msg_history[:mid]
|
|
287
|
+
second_half = msg_history[mid:]
|
|
288
|
+
mean_first = sum(first_half) / len(first_half)
|
|
289
|
+
mean_second = sum(second_half) / len(second_half)
|
|
290
|
+
if mean_first > 0:
|
|
291
|
+
growth = (mean_second - mean_first) / mean_first
|
|
292
|
+
msg_latency_signal = min(1.0, max(0.0, growth))
|
|
293
|
+
|
|
294
|
+
return min(1.0, (disconnect_rate + msg_latency_signal) / 2.0)
|
|
295
|
+
|
|
296
|
+
# ------------------------------------------------------------------
|
|
297
|
+
# Trend detection
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
def trend(self, endpoint: str) -> str:
|
|
301
|
+
"""Return trend direction for *endpoint*: improving / degrading / stable."""
|
|
302
|
+
history = self._score_history.get(endpoint, [])
|
|
303
|
+
if len(history) < 3:
|
|
304
|
+
return TrendDirection.STABLE
|
|
305
|
+
recent = history[-3:]
|
|
306
|
+
older = history[:-3] or [recent[0]]
|
|
307
|
+
mean_recent = sum(recent) / len(recent)
|
|
308
|
+
mean_older = sum(older) / len(older)
|
|
309
|
+
delta = mean_recent - mean_older
|
|
310
|
+
if delta > 0.05:
|
|
311
|
+
return TrendDirection.DEGRADING
|
|
312
|
+
if delta < -0.05:
|
|
313
|
+
return TrendDirection.IMPROVING
|
|
314
|
+
return TrendDirection.STABLE
|
|
315
|
+
|
|
316
|
+
# ------------------------------------------------------------------
|
|
317
|
+
# Report
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def _confidence_interval(self, endpoint: str) -> Tuple[float, float]:
|
|
321
|
+
"""Compute a 95% confidence interval for the risk score of *endpoint*.
|
|
322
|
+
|
|
323
|
+
Uses ``mean ± 1.96 * std / √n`` from the score history window.
|
|
324
|
+
|
|
325
|
+
Edge cases:
|
|
326
|
+
- ``n == 0``: return (0.0, 1.0) — widest possible range.
|
|
327
|
+
- ``n == 1``: return (score, score) — no variance estimable.
|
|
328
|
+
- ``std == 0``: return (score, score) — all identical scores.
|
|
329
|
+
|
|
330
|
+
Returns
|
|
331
|
+
-------
|
|
332
|
+
tuple[float, float]
|
|
333
|
+
``(ci_low, ci_high)`` clamped to [0.0, 1.0].
|
|
334
|
+
"""
|
|
335
|
+
history = self._score_history.get(endpoint, [])
|
|
336
|
+
n = len(history)
|
|
337
|
+
if n == 0:
|
|
338
|
+
return (0.0, 1.0)
|
|
339
|
+
if n == 1:
|
|
340
|
+
s = history[0]
|
|
341
|
+
return (s, s)
|
|
342
|
+
mean = sum(history) / n
|
|
343
|
+
variance = sum((x - mean) ** 2 for x in history) / (n - 1) # sample variance
|
|
344
|
+
std = math.sqrt(variance)
|
|
345
|
+
if std == 0.0:
|
|
346
|
+
return (mean, mean)
|
|
347
|
+
margin = 1.96 * std / math.sqrt(n)
|
|
348
|
+
return (
|
|
349
|
+
max(0.0, mean - margin),
|
|
350
|
+
min(1.0, mean + margin),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def get_risk_report(
|
|
354
|
+
self,
|
|
355
|
+
belief_states: Optional[List[BeliefState]] = None,
|
|
356
|
+
) -> List[Dict[str, Any]]:
|
|
357
|
+
"""Return a structured risk report for all tracked endpoints.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
list of dict
|
|
362
|
+
Each entry contains:
|
|
363
|
+
``endpoint``, ``risk_score``, ``fault_rate``, ``belief_signal``,
|
|
364
|
+
``latency_signal``, ``diversity_signal``, ``connection_signal``,
|
|
365
|
+
``disconnect_count``, ``trend``, ``run_count``, ``fault_count``,
|
|
366
|
+
``confidence_interval_low``, ``confidence_interval_high``,
|
|
367
|
+
``sample_count``.
|
|
368
|
+
Sorted by descending risk score.
|
|
369
|
+
"""
|
|
370
|
+
all_endpoints = set(self._run_counts.keys())
|
|
371
|
+
|
|
372
|
+
report = []
|
|
373
|
+
for ep in all_endpoints:
|
|
374
|
+
fault_rate = self._fault_rate(ep)
|
|
375
|
+
belief_signal = self._belief_signal(ep, belief_states)
|
|
376
|
+
latency_signal = self._latency_signal(ep)
|
|
377
|
+
diversity_signal = self._diversity_signal(ep)
|
|
378
|
+
connection_signal = self._connection_signal(ep)
|
|
379
|
+
risk_score = (
|
|
380
|
+
self._w_belief * belief_signal
|
|
381
|
+
+ self._w_fault * fault_rate
|
|
382
|
+
+ self._w_latency * latency_signal
|
|
383
|
+
+ self._w_diversity * diversity_signal
|
|
384
|
+
+ self._w_connection * connection_signal
|
|
385
|
+
)
|
|
386
|
+
risk_score = min(1.0, max(0.0, risk_score))
|
|
387
|
+
self._last_scores[ep] = risk_score
|
|
388
|
+
|
|
389
|
+
# Confidence interval: mean ± 1.96 * std / √n from score history
|
|
390
|
+
ci_low, ci_high = self._confidence_interval(ep)
|
|
391
|
+
|
|
392
|
+
report.append(
|
|
393
|
+
{
|
|
394
|
+
"endpoint": ep,
|
|
395
|
+
"risk_score": round(risk_score, 4),
|
|
396
|
+
"fault_rate": round(fault_rate, 4),
|
|
397
|
+
"belief_signal": round(belief_signal, 4),
|
|
398
|
+
"latency_signal": round(latency_signal, 4),
|
|
399
|
+
"diversity_signal": round(diversity_signal, 4),
|
|
400
|
+
"connection_signal": round(connection_signal, 4),
|
|
401
|
+
"disconnect_count": self._disconnect_counts.get(ep, 0),
|
|
402
|
+
"trend": self.trend(ep),
|
|
403
|
+
"run_count": self._run_counts.get(ep, 0),
|
|
404
|
+
"fault_count": self._fault_counts.get(ep, 0),
|
|
405
|
+
"confidence_interval_low": round(ci_low, 4),
|
|
406
|
+
"confidence_interval_high": round(ci_high, 4),
|
|
407
|
+
"sample_count": len(self._score_history.get(ep, [])),
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
report.sort(key=lambda x: x["risk_score"], reverse=True)
|
|
412
|
+
return report
|
|
413
|
+
|
|
414
|
+
def to_json(self, belief_states: Optional[List[BeliefState]] = None) -> str:
|
|
415
|
+
"""Return the risk report serialized as a JSON string."""
|
|
416
|
+
return json.dumps(self.get_risk_report(belief_states), indent=2)
|
|
417
|
+
|
|
418
|
+
# ------------------------------------------------------------------
|
|
419
|
+
# Convenience accessors
|
|
420
|
+
# ------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
def top_k(self, k: int, belief_states: Optional[List[BeliefState]] = None) -> List[Tuple[str, float]]:
|
|
423
|
+
"""Return the *k* highest-risk endpoints as (endpoint, score) tuples."""
|
|
424
|
+
report = self.get_risk_report(belief_states)
|
|
425
|
+
return [(r["endpoint"], r["risk_score"]) for r in report[:k]]
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def all_endpoints(self) -> List[str]:
|
|
429
|
+
"""All endpoints for which data has been recorded."""
|
|
430
|
+
return list(self._run_counts.keys())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""NAT reporting sub-package – unified test report generation."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from mannf.core.reporting.unified_report import UnifiedReportGenerator
|
|
11
|
+
|
|
12
|
+
__all__ = ["UnifiedReportGenerator"]
|