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,232 @@
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
+ """Data models for the regression record/replay system."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ @dataclass
16
+ class RequestRecord:
17
+ """Full details of a captured HTTP request."""
18
+
19
+ method: str
20
+ url: str
21
+ headers: Dict[str, str] = field(default_factory=dict)
22
+ body: Any = None
23
+ query_params: Optional[Dict[str, Any]] = None
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ return {
27
+ "method": self.method,
28
+ "url": self.url,
29
+ "headers": self.headers,
30
+ "body": self.body,
31
+ "query_params": self.query_params,
32
+ }
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: Dict[str, Any]) -> "RequestRecord":
36
+ return cls(
37
+ method=data["method"],
38
+ url=data["url"],
39
+ headers=data.get("headers") or {},
40
+ body=data.get("body"),
41
+ query_params=data.get("query_params"),
42
+ )
43
+
44
+
45
+ @dataclass
46
+ class ResponseRecord:
47
+ """Full details of a captured HTTP response."""
48
+
49
+ status_code: int
50
+ headers: Dict[str, str] = field(default_factory=dict)
51
+ body: Any = None
52
+ latency_ms: float = 0.0
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ return {
56
+ "status_code": self.status_code,
57
+ "headers": self.headers,
58
+ "body": self.body,
59
+ "latency_ms": self.latency_ms,
60
+ }
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: Dict[str, Any]) -> "ResponseRecord":
64
+ return cls(
65
+ status_code=data["status_code"],
66
+ headers=data.get("headers") or {},
67
+ body=data.get("body"),
68
+ latency_ms=data.get("latency_ms", 0.0),
69
+ )
70
+
71
+
72
+ @dataclass
73
+ class RegressionRecord:
74
+ """A single recorded request/response pair linked to a test case."""
75
+
76
+ test_case_id: str
77
+ request: RequestRecord
78
+ response: ResponseRecord
79
+ passed: bool
80
+ error: Optional[str] = None
81
+
82
+ def to_dict(self) -> Dict[str, Any]:
83
+ return {
84
+ "test_case_id": self.test_case_id,
85
+ "request": self.request.to_dict(),
86
+ "response": self.response.to_dict(),
87
+ "passed": self.passed,
88
+ "error": self.error,
89
+ }
90
+
91
+ @classmethod
92
+ def from_dict(cls, data: Dict[str, Any]) -> "RegressionRecord":
93
+ return cls(
94
+ test_case_id=data["test_case_id"],
95
+ request=RequestRecord.from_dict(data["request"]),
96
+ response=ResponseRecord.from_dict(data["response"]),
97
+ passed=data["passed"],
98
+ error=data.get("error"),
99
+ )
100
+
101
+
102
+ @dataclass
103
+ class RegressionBaseline:
104
+ """A portable regression baseline file containing all recorded request/response pairs.
105
+
106
+ Serialises to/from the standard NAT regression JSON format::
107
+
108
+ {
109
+ "version": "1.0",
110
+ "recorded_at": "2026-03-21T12:00:00Z",
111
+ "spec": "openapi.yaml",
112
+ "base_url": "https://api.example.com",
113
+ "records": [...]
114
+ }
115
+ """
116
+
117
+ version: str = "1.0"
118
+ recorded_at: str = ""
119
+ spec: Optional[str] = None
120
+ base_url: str = ""
121
+ records: List[RegressionRecord] = field(default_factory=list)
122
+
123
+ def to_dict(self) -> Dict[str, Any]:
124
+ return {
125
+ "version": self.version,
126
+ "recorded_at": self.recorded_at,
127
+ "spec": self.spec,
128
+ "base_url": self.base_url,
129
+ "records": [r.to_dict() for r in self.records],
130
+ }
131
+
132
+ def save(self, path: str) -> None:
133
+ """Serialise the baseline to a JSON file at *path*."""
134
+ with open(path, "w", encoding="utf-8") as fh:
135
+ json.dump(self.to_dict(), fh, indent=2)
136
+
137
+ @classmethod
138
+ def from_dict(cls, data: Dict[str, Any]) -> "RegressionBaseline":
139
+ return cls(
140
+ version=data.get("version", "1.0"),
141
+ recorded_at=data.get("recorded_at", ""),
142
+ spec=data.get("spec"),
143
+ base_url=data.get("base_url", ""),
144
+ records=[RegressionRecord.from_dict(r) for r in data.get("records", [])],
145
+ )
146
+
147
+ @classmethod
148
+ def load(cls, path: str) -> "RegressionBaseline":
149
+ """Load a baseline from a JSON file at *path*."""
150
+ with open(path, encoding="utf-8") as fh:
151
+ data = json.load(fh)
152
+ return cls.from_dict(data)
153
+
154
+
155
+ @dataclass
156
+ class FieldChange:
157
+ """A single changed field between baseline and replay."""
158
+
159
+ field: str
160
+ baseline: Any
161
+ replay: Any
162
+
163
+ def to_dict(self) -> Dict[str, Any]:
164
+ return {
165
+ "field": self.field,
166
+ "baseline": self.baseline,
167
+ "replay": self.replay,
168
+ }
169
+
170
+
171
+ @dataclass
172
+ class RecordDiff:
173
+ """Diff result for a single test case."""
174
+
175
+ test_case_id: str
176
+ endpoint: str
177
+ changes: List[FieldChange] = field(default_factory=list)
178
+
179
+ def to_dict(self) -> Dict[str, Any]:
180
+ return {
181
+ "test_case_id": self.test_case_id,
182
+ "endpoint": self.endpoint,
183
+ "changes": [c.to_dict() for c in self.changes],
184
+ }
185
+
186
+
187
+ @dataclass
188
+ class RegressionDiff:
189
+ """Full diff report comparing a baseline to a replay run.
190
+
191
+ Serialises to the standard NAT regression diff JSON format::
192
+
193
+ {
194
+ "summary": {"total": 10, "unchanged": 8, "changed": 2, "new": 0, "removed": 0},
195
+ "changes": [...]
196
+ }
197
+ """
198
+
199
+ summary: Dict[str, int] = field(default_factory=dict)
200
+ changes: List[RecordDiff] = field(default_factory=list)
201
+
202
+ def to_dict(self) -> Dict[str, Any]:
203
+ return {
204
+ "summary": self.summary,
205
+ "changes": [c.to_dict() for c in self.changes],
206
+ }
207
+
208
+ def save(self, path: str) -> None:
209
+ """Write the diff report to a JSON file at *path*."""
210
+ with open(path, "w", encoding="utf-8") as fh:
211
+ json.dump(self.to_dict(), fh, indent=2)
212
+
213
+ def human_readable(self) -> str:
214
+ """Return a human-readable diff summary string."""
215
+ lines = []
216
+ s = self.summary
217
+ lines.append("=" * 60)
218
+ lines.append(" Regression Diff Report")
219
+ lines.append("=" * 60)
220
+ lines.append(f" Total: {s.get('total', 0)}")
221
+ lines.append(f" Unchanged: {s.get('unchanged', 0)}")
222
+ lines.append(f" Changed: {s.get('changed', 0)}")
223
+ lines.append(f" New: {s.get('new', 0)}")
224
+ lines.append(f" Removed: {s.get('removed', 0)}")
225
+ lines.append("=" * 60)
226
+ for diff in self.changes:
227
+ lines.append(f"\n [{diff.test_case_id}] {diff.endpoint}")
228
+ for chg in diff.changes:
229
+ lines.append(f" {chg.field}:")
230
+ lines.append(f" baseline: {chg.baseline!r}")
231
+ lines.append(f" replay: {chg.replay!r}")
232
+ return "\n".join(lines)
@@ -0,0 +1,124 @@
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
+ """RegressionRecorder — hooks into SUT execute methods to capture request/response pairs."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, Optional
12
+
13
+ from mannf.product.regression.masking import mask_sensitive
14
+ from mannf.product.regression.models import (
15
+ RegressionBaseline,
16
+ RegressionRecord,
17
+ RequestRecord,
18
+ ResponseRecord,
19
+ )
20
+
21
+
22
+ class RegressionRecorder:
23
+ """Collects :class:`~mannf.product.regression.models.RegressionRecord` objects during a test run.
24
+
25
+ Pass an instance to :class:`~mannf.product.integrations.http_sut.HttpApiSUT` or
26
+ :class:`~mannf.product.integrations.graphql_sut.GraphQLSUT` via the ``recorder``
27
+ constructor parameter. After the run call :meth:`baseline` to obtain the
28
+ completed :class:`~mannf.product.regression.models.RegressionBaseline`.
29
+
30
+ Parameters
31
+ ----------
32
+ spec:
33
+ Path or identifier of the spec file used in this run.
34
+ base_url:
35
+ Root URL of the API under test.
36
+ mask_sensitive_data:
37
+ When ``True``, auth headers and token values are replaced with
38
+ ``***MASKED***`` before being stored.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ spec: Optional[str] = None,
44
+ base_url: str = "",
45
+ mask_sensitive_data: bool = False,
46
+ ) -> None:
47
+ self.spec = spec
48
+ self.base_url = base_url
49
+ self.mask_sensitive_data = mask_sensitive_data
50
+ self._records: list[RegressionRecord] = []
51
+
52
+ # ------------------------------------------------------------------
53
+ # Hook called by SUT implementations
54
+ # ------------------------------------------------------------------
55
+
56
+ def record(
57
+ self,
58
+ *,
59
+ test_case_id: str,
60
+ method: str,
61
+ url: str,
62
+ request_headers: Dict[str, str],
63
+ request_body: Any,
64
+ query_params: Optional[Dict[str, Any]],
65
+ status_code: int,
66
+ response_headers: Dict[str, str],
67
+ response_body: Any,
68
+ latency_ms: float,
69
+ passed: bool,
70
+ error: Optional[str],
71
+ ) -> None:
72
+ """Append one request/response pair to the in-memory log."""
73
+ if self.mask_sensitive_data:
74
+ request_headers, request_body, query_params = mask_sensitive(
75
+ headers=request_headers,
76
+ body=request_body,
77
+ query_params=query_params,
78
+ )
79
+ response_headers, response_body, _ = mask_sensitive(
80
+ headers=response_headers,
81
+ body=response_body,
82
+ )
83
+
84
+ self._records.append(
85
+ RegressionRecord(
86
+ test_case_id=test_case_id,
87
+ request=RequestRecord(
88
+ method=method,
89
+ url=url,
90
+ headers=request_headers,
91
+ body=request_body,
92
+ query_params=query_params,
93
+ ),
94
+ response=ResponseRecord(
95
+ status_code=status_code,
96
+ headers=response_headers,
97
+ body=response_body,
98
+ latency_ms=latency_ms,
99
+ ),
100
+ passed=passed,
101
+ error=error,
102
+ )
103
+ )
104
+
105
+ # ------------------------------------------------------------------
106
+ # Output
107
+ # ------------------------------------------------------------------
108
+
109
+ def baseline(self) -> RegressionBaseline:
110
+ """Return a :class:`~mannf.product.regression.models.RegressionBaseline` for the current run."""
111
+ return RegressionBaseline(
112
+ version="1.0",
113
+ recorded_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
114
+ spec=self.spec,
115
+ base_url=self.base_url,
116
+ records=list(self._records),
117
+ )
118
+
119
+ def save(self, path: str) -> None:
120
+ """Write the current baseline to *path* as a JSON file."""
121
+ self.baseline().save(path)
122
+
123
+ def __len__(self) -> int:
124
+ return len(self._records)
@@ -0,0 +1,168 @@
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
+ """RegressionReplayer — re-executes a previously recorded baseline against the live API."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import time
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import httpx
15
+
16
+ from mannf.product.integrations.auth import AuthStrategy, NoAuth
17
+ from mannf.product.regression.models import (
18
+ RegressionBaseline,
19
+ RegressionRecord,
20
+ RequestRecord,
21
+ ResponseRecord,
22
+ )
23
+ from mannf.product.regression.recorder import RegressionRecorder
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class RegressionReplayer:
29
+ """Re-run all requests from a :class:`~mannf.product.regression.models.RegressionBaseline`.
30
+
31
+ For each record in the baseline the replayer constructs an identical HTTP
32
+ request, fires it at the (potentially changed) API, and stores the new
33
+ response. The resulting :class:`~mannf.product.regression.models.RegressionBaseline`
34
+ can then be compared against the original with
35
+ :class:`~mannf.product.regression.differ.RegressionDiffer`.
36
+
37
+ Parameters
38
+ ----------
39
+ baseline:
40
+ The loaded regression baseline to replay.
41
+ auth:
42
+ Optional :class:`~mannf.product.integrations.auth.AuthStrategy` applied to
43
+ every replayed request (uses :class:`~mannf.product.integrations.auth.NoAuth`
44
+ by default).
45
+ timeout:
46
+ HTTP request timeout in seconds (default: 10).
47
+ mask_sensitive_data:
48
+ When ``True``, mask auth headers / token values in the replay
49
+ recording.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ baseline: RegressionBaseline,
55
+ auth: Optional[AuthStrategy] = None,
56
+ timeout: float = 10.0,
57
+ mask_sensitive_data: bool = False,
58
+ ) -> None:
59
+ self.baseline = baseline
60
+ self.auth: AuthStrategy = auth or NoAuth()
61
+ self.timeout = timeout
62
+ self.mask_sensitive_data = mask_sensitive_data
63
+
64
+ async def replay(self) -> RegressionBaseline:
65
+ """Execute all baseline requests and return a new baseline with replay responses.
66
+
67
+ Returns
68
+ -------
69
+ RegressionBaseline
70
+ A new baseline whose records contain the *replay* responses.
71
+ ``recorded_at`` is set to the current time.
72
+ """
73
+ recorder = RegressionRecorder(
74
+ spec=self.baseline.spec,
75
+ base_url=self.baseline.base_url,
76
+ mask_sensitive_data=self.mask_sensitive_data,
77
+ )
78
+
79
+ for record in self.baseline.records:
80
+ await self._replay_one(record, recorder)
81
+
82
+ return recorder.baseline()
83
+
84
+ async def _replay_one(
85
+ self, record: RegressionRecord, recorder: RegressionRecorder
86
+ ) -> None:
87
+ req: RequestRecord = record.request
88
+ request_kwargs: Dict[str, Any] = {
89
+ "method": req.method,
90
+ "url": req.url,
91
+ "headers": dict(req.headers),
92
+ "timeout": self.timeout,
93
+ }
94
+ if req.query_params:
95
+ request_kwargs["params"] = req.query_params
96
+ if req.body is not None:
97
+ if isinstance(req.body, (dict, list)):
98
+ request_kwargs["json"] = req.body
99
+ else:
100
+ request_kwargs["content"] = req.body
101
+
102
+ # Apply auth
103
+ request_kwargs = await self.auth.apply(request_kwargs)
104
+
105
+ start = time.monotonic()
106
+ try:
107
+ async with httpx.AsyncClient() as client:
108
+ response = await client.request(**request_kwargs)
109
+ elapsed_ms = (time.monotonic() - start) * 1000.0
110
+
111
+ try:
112
+ body: Any = response.json()
113
+ except Exception:
114
+ body = response.text
115
+
116
+ # Re-use original pass criteria: 2xx means pass
117
+ passed = 200 <= response.status_code < 300
118
+ error: Optional[str] = None if passed else f"Unexpected status {response.status_code}"
119
+
120
+ recorder.record(
121
+ test_case_id=record.test_case_id,
122
+ method=req.method,
123
+ url=req.url,
124
+ request_headers=dict(req.headers),
125
+ request_body=req.body,
126
+ query_params=req.query_params,
127
+ status_code=response.status_code,
128
+ response_headers=dict(response.headers),
129
+ response_body=body,
130
+ latency_ms=elapsed_ms,
131
+ passed=passed,
132
+ error=error,
133
+ )
134
+
135
+ except httpx.TimeoutException as exc:
136
+ elapsed_ms = (time.monotonic() - start) * 1000.0
137
+ logger.warning("RegressionReplayer: timeout replaying %s %s — %s", req.method, req.url, exc)
138
+ recorder.record(
139
+ test_case_id=record.test_case_id,
140
+ method=req.method,
141
+ url=req.url,
142
+ request_headers=dict(req.headers),
143
+ request_body=req.body,
144
+ query_params=req.query_params,
145
+ status_code=0,
146
+ response_headers={},
147
+ response_body=None,
148
+ latency_ms=elapsed_ms,
149
+ passed=False,
150
+ error=f"Request timed out: {exc}",
151
+ )
152
+ except httpx.RequestError as exc:
153
+ elapsed_ms = (time.monotonic() - start) * 1000.0
154
+ logger.warning("RegressionReplayer: connection error for %s %s — %s", req.method, req.url, exc)
155
+ recorder.record(
156
+ test_case_id=record.test_case_id,
157
+ method=req.method,
158
+ url=req.url,
159
+ request_headers=dict(req.headers),
160
+ request_body=req.body,
161
+ query_params=req.query_params,
162
+ status_code=0,
163
+ response_headers={},
164
+ response_body=None,
165
+ latency_ms=elapsed_ms,
166
+ passed=False,
167
+ error=f"Connection error: {exc}",
168
+ )
@@ -0,0 +1,10 @@
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 report generators — PDF and other exportable formats."""
7
+
8
+ from mannf.product.reports.pdf import PDFReportGenerator
9
+
10
+ __all__ = ["PDFReportGenerator"]
@@ -0,0 +1,132 @@
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
+ """PDF report generator for NAT scan exports.
7
+
8
+ Uses Playwright's ``page.pdf()`` to render the existing unified HTML report
9
+ as a PDF document. Playwright is already a declared dependency of the NAT
10
+ framework (used by browser-based test agents), so no additional package is
11
+ required.
12
+
13
+ Enterprise white-label option:
14
+ When *whitelabel=True* is passed to :meth:`PDFReportGenerator.generate`,
15
+ all occurrences of the NAT brand strings are stripped from the HTML before
16
+ rendering, producing a clean, unbranded PDF suitable for enterprise
17
+ stakeholder distribution.
18
+
19
+ Usage::
20
+
21
+ from mannf.product.reports.pdf import PDFReportGenerator
22
+
23
+ generator = PDFReportGenerator()
24
+ pdf_bytes = await generator.generate(html_content)
25
+
26
+ # Enterprise white-label (strip NAT branding):
27
+ pdf_bytes = await generator.generate(html_content, whitelabel=True)
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import re
33
+ from typing import Any
34
+
35
+ # NAT brand strings to strip when producing white-label PDFs.
36
+ _WHITELABEL_REPLACEMENTS: list[tuple[str, str]] = [
37
+ # Footer / title references
38
+ (r"Generated by NAT Framework\s*&mdash;\s*Neural Agent Testing\s*&mdash;\s*", "Generated "),
39
+ (r"NAT Unified Test Report", "Test Report"),
40
+ (r"NAT Scan Report", "Scan Report"),
41
+ (r"NeuroAgentTest\s*\([^)]*\)", ""),
42
+ (r"Neural Agent Testing Framework", ""),
43
+ (r"NAT Framework", ""),
44
+ # HTML <title> tag content
45
+ (r"(<title>)NAT\s+", r"\1"),
46
+ # Header h1 with "NAT" prefix
47
+ (r"<h1>NAT\s+", "<h1>"),
48
+ ]
49
+
50
+
51
+ class PDFReportGenerator:
52
+ """Convert an HTML scan report to a PDF document using Playwright.
53
+
54
+ This class wraps Playwright's ``page.pdf()`` method to produce a
55
+ print-quality PDF from the same self-contained HTML report that is
56
+ served by ``GET /api/v1/scan/{scan_id}/export?format=html``.
57
+
58
+ Enterprise tenants may request white-label output (NAT branding stripped)
59
+ by passing ``whitelabel=True`` to :meth:`generate`.
60
+
61
+ Raises:
62
+ RuntimeError: If Playwright is not installed or the browser binary
63
+ has not been installed via ``playwright install``.
64
+ """
65
+
66
+ # ------------------------------------------------------------------
67
+ # Public API
68
+ # ------------------------------------------------------------------
69
+
70
+ async def generate(self, html: str, whitelabel: bool = False) -> bytes:
71
+ """Render *html* as a PDF document and return the raw PDF bytes.
72
+
73
+ Parameters
74
+ ----------
75
+ html:
76
+ A self-contained HTML document (no external dependencies) to
77
+ render. Typically the output of
78
+ :func:`~mannf.product.dashboard.app._render_export_html` or
79
+ :meth:`~mannf.core.reporting.unified_report.UnifiedReportGenerator.generate_html`.
80
+ whitelabel:
81
+ When ``True``, NAT brand strings are stripped from the HTML
82
+ before rendering. Intended for Enterprise plan tenants.
83
+
84
+ Returns
85
+ -------
86
+ bytes
87
+ Raw PDF bytes suitable for returning as an HTTP response or
88
+ writing to disk.
89
+
90
+ Raises
91
+ ------
92
+ RuntimeError
93
+ If the ``playwright`` package is not importable or the required
94
+ browser binary is missing.
95
+ """
96
+ try:
97
+ from playwright.async_api import async_playwright # noqa: PLC0415
98
+ except ImportError as exc:
99
+ raise RuntimeError(
100
+ "Playwright is required for PDF export. "
101
+ "Install it with: pip install playwright && playwright install chromium"
102
+ ) from exc
103
+
104
+ if whitelabel:
105
+ html = self._strip_branding(html)
106
+
107
+ async with async_playwright() as pw:
108
+ browser = await pw.chromium.launch()
109
+ try:
110
+ page = await browser.new_page()
111
+ await page.set_content(html, wait_until="networkidle")
112
+ pdf_bytes: bytes = await page.pdf(
113
+ format="A4",
114
+ print_background=True,
115
+ margin={"top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm"},
116
+ )
117
+ finally:
118
+ await browser.close()
119
+
120
+ return pdf_bytes
121
+
122
+ # ------------------------------------------------------------------
123
+ # Internal helpers
124
+ # ------------------------------------------------------------------
125
+
126
+ @staticmethod
127
+ def _strip_branding(html: str) -> str:
128
+ """Remove NAT brand strings from *html* for white-label output."""
129
+ result = html
130
+ for pattern, replacement in _WHITELABEL_REPLACEMENTS:
131
+ result = re.sub(pattern, replacement, result)
132
+ return result