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.
Files changed (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. 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"]