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,251 @@
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
+ """Schema differ for OpenAPI specs."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional, Set, Tuple
12
+
13
+ from mannf.product.healing.models import EndpointChange, FieldChange, SchemaDiff
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _HTTP_METHODS = ("get", "post", "put", "patch", "delete", "head", "options")
18
+
19
+
20
+ class SchemaDiffer:
21
+ """Compare two OpenAPI / Swagger specs and return a structured :class:`SchemaDiff`."""
22
+
23
+ def diff(self, old_spec: Dict[str, Any], new_spec: Dict[str, Any]) -> SchemaDiff:
24
+ """Compare *old_spec* and *new_spec* and return a :class:`SchemaDiff`."""
25
+ old_endpoints = self._extract_endpoints(old_spec)
26
+ new_endpoints = self._extract_endpoints(new_spec)
27
+
28
+ old_keys: Set[Tuple[str, str]] = set(old_endpoints.keys())
29
+ new_keys: Set[Tuple[str, str]] = set(new_endpoints.keys())
30
+
31
+ added_keys = new_keys - old_keys
32
+ removed_keys = old_keys - new_keys
33
+ common_keys = old_keys & new_keys
34
+
35
+ added_endpoints = [
36
+ EndpointChange(method=method, path=path, change_type="added")
37
+ for method, path in sorted(added_keys)
38
+ ]
39
+ removed_endpoints = [
40
+ EndpointChange(method=method, path=path, change_type="removed")
41
+ for method, path in sorted(removed_keys)
42
+ ]
43
+ modified_endpoints = []
44
+ for method, path in sorted(common_keys):
45
+ old_op = old_endpoints[(method, path)]
46
+ new_op = new_endpoints[(method, path)]
47
+ changes = self._diff_operation(method, path, old_op, new_op, old_spec, new_spec)
48
+ if changes:
49
+ modified_endpoints.append(
50
+ EndpointChange(method=method, path=path, change_type="modified", details=changes)
51
+ )
52
+
53
+ result = SchemaDiff(
54
+ added_endpoints=added_endpoints,
55
+ removed_endpoints=removed_endpoints,
56
+ modified_endpoints=modified_endpoints,
57
+ )
58
+ result.summary = {
59
+ "added": len(added_endpoints),
60
+ "removed": len(removed_endpoints),
61
+ "modified": len(modified_endpoints),
62
+ "total_changes": len(added_endpoints) + len(removed_endpoints) + len(modified_endpoints),
63
+ }
64
+ return result
65
+
66
+ def _extract_endpoints(self, spec: Dict[str, Any]) -> Dict[Tuple[str, str], Dict[str, Any]]:
67
+ """Return mapping of (method, path) -> operation dict."""
68
+ endpoints: Dict[Tuple[str, str], Dict[str, Any]] = {}
69
+ for path, path_item in spec.get("paths", {}).items():
70
+ if not isinstance(path_item, dict):
71
+ continue
72
+ for method in _HTTP_METHODS:
73
+ op = path_item.get(method)
74
+ if isinstance(op, dict):
75
+ endpoints[(method.upper(), path)] = op
76
+ return endpoints
77
+
78
+ def _diff_operation(
79
+ self,
80
+ method: str,
81
+ path: str,
82
+ old_op: Dict[str, Any],
83
+ new_op: Dict[str, Any],
84
+ old_spec: Dict[str, Any],
85
+ new_spec: Dict[str, Any],
86
+ ) -> List[FieldChange]:
87
+ """Return field-level changes between two operations."""
88
+ changes: List[FieldChange] = []
89
+
90
+ # Compare parameters
91
+ old_params = self._params_by_name(old_op.get("parameters", []), old_spec)
92
+ new_params = self._params_by_name(new_op.get("parameters", []), new_spec)
93
+
94
+ for name in set(old_params) - set(new_params):
95
+ changes.append(FieldChange(
96
+ field_path=f"parameters.{name}",
97
+ change_type="removed",
98
+ old_value=old_params[name],
99
+ new_value=None,
100
+ ))
101
+ for name in set(new_params) - set(old_params):
102
+ changes.append(FieldChange(
103
+ field_path=f"parameters.{name}",
104
+ change_type="added",
105
+ old_value=None,
106
+ new_value=new_params[name],
107
+ ))
108
+ for name in set(old_params) & set(new_params):
109
+ old_p = old_params[name]
110
+ new_p = new_params[name]
111
+ old_type = (old_p.get("schema") or {}).get("type")
112
+ new_type = (new_p.get("schema") or {}).get("type")
113
+ if old_type != new_type:
114
+ changes.append(FieldChange(
115
+ field_path=f"parameters.{name}.type",
116
+ change_type="type_changed",
117
+ old_value=old_type,
118
+ new_value=new_type,
119
+ ))
120
+
121
+ # Compare request body schema
122
+ old_body = self._get_body_schema(old_op, old_spec)
123
+ new_body = self._get_body_schema(new_op, new_spec)
124
+ if old_body or new_body:
125
+ body_changes = self._diff_schemas("requestBody", old_body or {}, new_body or {})
126
+ changes.extend(body_changes)
127
+
128
+ # Compare response schemas (200, 201, 2xx)
129
+ old_resp = self._get_response_schema(old_op, old_spec)
130
+ new_resp = self._get_response_schema(new_op, new_spec)
131
+ if old_resp or new_resp:
132
+ resp_changes = self._diff_schemas("response", old_resp or {}, new_resp or {})
133
+ changes.extend(resp_changes)
134
+
135
+ return changes
136
+
137
+ def _params_by_name(
138
+ self, params: List[Dict[str, Any]], spec: Dict[str, Any]
139
+ ) -> Dict[str, Dict[str, Any]]:
140
+ result: Dict[str, Dict[str, Any]] = {}
141
+ for p in params:
142
+ if "$ref" in p:
143
+ p = _resolve_ref(spec, p["$ref"])
144
+ name = p.get("name", "")
145
+ if name:
146
+ result[name] = p
147
+ return result
148
+
149
+ def _get_body_schema(
150
+ self, op: Dict[str, Any], spec: Dict[str, Any]
151
+ ) -> Optional[Dict[str, Any]]:
152
+ # OpenAPI 3
153
+ rb = op.get("requestBody", {})
154
+ if rb:
155
+ content = rb.get("content", {})
156
+ for mt in ("application/json", "application/x-www-form-urlencoded"):
157
+ if mt in content:
158
+ s = content[mt].get("schema", {})
159
+ return _resolve_ref(spec, s["$ref"]) if "$ref" in s else s
160
+ # Swagger 2
161
+ for p in op.get("parameters", []):
162
+ if "$ref" in p:
163
+ p = _resolve_ref(spec, p["$ref"])
164
+ if p.get("in") == "body":
165
+ s = p.get("schema", {})
166
+ return _resolve_ref(spec, s["$ref"]) if "$ref" in s else s
167
+ return None
168
+
169
+ def _get_response_schema(
170
+ self, op: Dict[str, Any], spec: Dict[str, Any]
171
+ ) -> Optional[Dict[str, Any]]:
172
+ responses = op.get("responses", {})
173
+ for code in ("200", "201", "2xx", "default"):
174
+ resp = responses.get(code, {})
175
+ if resp:
176
+ content = resp.get("content", {})
177
+ if content:
178
+ for mt in ("application/json",):
179
+ if mt in content:
180
+ s = content[mt].get("schema", {})
181
+ return _resolve_ref(spec, s["$ref"]) if "$ref" in s else s
182
+ schema = resp.get("schema", {})
183
+ if schema:
184
+ return _resolve_ref(spec, schema["$ref"]) if "$ref" in schema else schema
185
+ return None
186
+
187
+ def _diff_schemas(
188
+ self, prefix: str, old_schema: Dict[str, Any], new_schema: Dict[str, Any]
189
+ ) -> List[FieldChange]:
190
+ changes: List[FieldChange] = []
191
+ old_props = old_schema.get("properties", {})
192
+ new_props = new_schema.get("properties", {})
193
+
194
+ for name in set(old_props) - set(new_props):
195
+ changes.append(FieldChange(
196
+ field_path=f"{prefix}.properties.{name}",
197
+ change_type="removed",
198
+ old_value=old_props[name],
199
+ new_value=None,
200
+ ))
201
+ for name in set(new_props) - set(old_props):
202
+ changes.append(FieldChange(
203
+ field_path=f"{prefix}.properties.{name}",
204
+ change_type="added",
205
+ old_value=None,
206
+ new_value=new_props[name],
207
+ ))
208
+ for name in set(old_props) & set(new_props):
209
+ old_t = old_props[name].get("type")
210
+ new_t = new_props[name].get("type")
211
+ if old_t != new_t:
212
+ changes.append(FieldChange(
213
+ field_path=f"{prefix}.properties.{name}.type",
214
+ change_type="type_changed",
215
+ old_value=old_t,
216
+ new_value=new_t,
217
+ ))
218
+
219
+ # Compare required fields
220
+ old_req = set(old_schema.get("required", []))
221
+ new_req = set(new_schema.get("required", []))
222
+ for name in old_req - new_req:
223
+ changes.append(FieldChange(
224
+ field_path=f"{prefix}.required.{name}",
225
+ change_type="constraint_changed",
226
+ old_value="required",
227
+ new_value="optional",
228
+ ))
229
+ for name in new_req - old_req:
230
+ changes.append(FieldChange(
231
+ field_path=f"{prefix}.required.{name}",
232
+ change_type="constraint_changed",
233
+ old_value="optional",
234
+ new_value="required",
235
+ ))
236
+
237
+ return changes
238
+
239
+
240
+ def _resolve_ref(spec: Dict[str, Any], ref: str) -> Dict[str, Any]:
241
+ """Resolve a ``$ref`` pointer within *spec*."""
242
+ if not ref.startswith("#/"):
243
+ return {}
244
+ parts = ref.lstrip("#/").split("/")
245
+ node: Any = spec
246
+ for part in parts:
247
+ part = part.replace("~1", "/").replace("~0", "~")
248
+ if not isinstance(node, dict) or part not in node:
249
+ return {}
250
+ node = node[part]
251
+ return node if isinstance(node, dict) else {}
@@ -0,0 +1,77 @@
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
+ """Ingestor plugins package — public API."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from mannf.product.ingestors.base import IngestorPlugin
11
+ from mannf.product.ingestors.models import (
12
+ IngestedEndpoint,
13
+ IngestedTestCase,
14
+ IngestorSource,
15
+ IngestResult,
16
+ )
17
+ from mannf.product.ingestors.loader import (
18
+ get_ingestor_by_name,
19
+ get_ingestor_for_source,
20
+ load_all_ingestors,
21
+ load_ingestors_from_directory,
22
+ load_ingestors_from_entry_points,
23
+ )
24
+ from mannf.product.ingestors.bgstm_ingestor import BGSTMIngestor
25
+ from mannf.product.ingestors.curl_ingestor import CurlIngestor
26
+ from mannf.product.ingestors.cypress_ingestor import CypressIngestor
27
+ from mannf.product.ingestors.gherkin_ingestor import GherkinIngestor
28
+ from mannf.product.ingestors.graphql_ingestor import GraphQLIngestor
29
+ from mannf.product.ingestors.grpc_ingestor import GrpcIngestor
30
+ from mannf.product.ingestors.har_ingestor import HARIngestor
31
+ from mannf.product.ingestors.openapi_ingestor import OpenAPIIngestor
32
+ from mannf.product.ingestors.playwright_ingestor import PlaywrightIngestor
33
+ from mannf.product.ingestors.postman_ingestor import PostmanIngestor
34
+ from mannf.product.ingestors.traffic_ingestor import TrafficIngestor
35
+ from mannf.product.ingestors.websocket_ingestor import WebSocketIngestor
36
+
37
+ # Built-in ingestors that are always available without entry-point registration.
38
+ BUILTIN_INGESTORS: list[IngestorPlugin] = [
39
+ OpenAPIIngestor(),
40
+ PostmanIngestor(),
41
+ GraphQLIngestor(),
42
+ GrpcIngestor(),
43
+ BGSTMIngestor(),
44
+ HARIngestor(),
45
+ CurlIngestor(),
46
+ GherkinIngestor(),
47
+ PlaywrightIngestor(),
48
+ CypressIngestor(),
49
+ TrafficIngestor(),
50
+ WebSocketIngestor(),
51
+ ]
52
+
53
+ __all__ = [
54
+ "IngestorPlugin",
55
+ "IngestorSource",
56
+ "IngestedEndpoint",
57
+ "IngestedTestCase",
58
+ "IngestResult",
59
+ "BGSTMIngestor",
60
+ "CurlIngestor",
61
+ "CypressIngestor",
62
+ "GherkinIngestor",
63
+ "GraphQLIngestor",
64
+ "GrpcIngestor",
65
+ "HARIngestor",
66
+ "OpenAPIIngestor",
67
+ "PlaywrightIngestor",
68
+ "PostmanIngestor",
69
+ "TrafficIngestor",
70
+ "WebSocketIngestor",
71
+ "BUILTIN_INGESTORS",
72
+ "load_all_ingestors",
73
+ "load_ingestors_from_directory",
74
+ "load_ingestors_from_entry_points",
75
+ "get_ingestor_by_name",
76
+ "get_ingestor_for_source",
77
+ ]
@@ -0,0 +1,256 @@
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
+ """Abstract base class for ingestor plugins.
7
+
8
+ All ingestor plugins must subclass :class:`IngestorPlugin` and implement the
9
+ three abstract methods: :meth:`ingest`, :meth:`validate_config`, and
10
+ :meth:`can_handle`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from abc import ABC, abstractmethod
17
+ from pathlib import Path
18
+
19
+ from mannf.product.ingestors.models import (
20
+ IngestedEndpoint,
21
+ IngestedTestCase,
22
+ IngestorSource,
23
+ IngestResult,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class IngestorPlugin(ABC):
30
+ """Abstract base class for ingestor plugins.
31
+
32
+ Subclasses must define the class-level attributes ``name``,
33
+ ``display_name``, ``description``, ``supported_extensions``, and
34
+ ``supported_formats``, and implement the abstract methods :meth:`ingest`,
35
+ :meth:`validate_config`, and :meth:`can_handle`.
36
+
37
+ Example::
38
+
39
+ class MyIngestor(IngestorPlugin):
40
+ name = "myformat"
41
+ display_name = "My Format"
42
+ description = "Ingest MyFormat specs."
43
+ supported_extensions = [".myfmt"]
44
+ supported_formats = ["myformat"]
45
+
46
+ async def ingest(self, source, config):
47
+ content = self._read_source(source)
48
+ # ... parse content ...
49
+ return IngestResult(success=True, source=source)
50
+
51
+ def validate_config(self, config):
52
+ return []
53
+
54
+ def can_handle(self, source):
55
+ return source.format in self.supported_formats
56
+ """
57
+
58
+ name: str # e.g., "openapi"
59
+ display_name: str # e.g., "OpenAPI/Swagger"
60
+ description: str # one-line description
61
+ supported_extensions: list[str] # e.g., [".yaml", ".yml", ".json"]
62
+ supported_formats: list[str] # e.g., ["openapi", "swagger"]
63
+
64
+ # ------------------------------------------------------------------
65
+ # Abstract methods
66
+ # ------------------------------------------------------------------
67
+
68
+ @abstractmethod
69
+ async def ingest(self, source: IngestorSource, config: dict) -> IngestResult:
70
+ """Ingest the given source and return a unified :class:`IngestResult`.
71
+
72
+ Parameters
73
+ ----------
74
+ source:
75
+ Metadata and content of the source to ingest.
76
+ config:
77
+ Plugin-specific configuration dict (keys validated by
78
+ :meth:`validate_config`).
79
+
80
+ Returns
81
+ -------
82
+ IngestResult
83
+ The result of the ingestion, including extracted endpoints and
84
+ test cases.
85
+ """
86
+ ...
87
+
88
+ @abstractmethod
89
+ def validate_config(self, config: dict) -> list[str]:
90
+ """Validate plugin-specific configuration.
91
+
92
+ Parameters
93
+ ----------
94
+ config:
95
+ Configuration dict to validate.
96
+
97
+ Returns
98
+ -------
99
+ list[str]
100
+ A list of human-readable error strings. An empty list means the
101
+ config is valid.
102
+ """
103
+ ...
104
+
105
+ @abstractmethod
106
+ def can_handle(self, source: IngestorSource) -> bool:
107
+ """Return True if this plugin can handle the given source.
108
+
109
+ Parameters
110
+ ----------
111
+ source:
112
+ The source to evaluate.
113
+
114
+ Returns
115
+ -------
116
+ bool
117
+ True if this plugin is capable of ingesting the source.
118
+ """
119
+ ...
120
+
121
+ # ------------------------------------------------------------------
122
+ # Concrete helper methods
123
+ # ------------------------------------------------------------------
124
+
125
+ def _read_source(self, source: IngestorSource) -> str:
126
+ """Read and return the raw content from *source*.
127
+
128
+ Tries, in order:
129
+ 1. ``source.raw_content`` — returned as-is if present.
130
+ 2. ``source.path`` — read from the local filesystem.
131
+
132
+ Parameters
133
+ ----------
134
+ source:
135
+ The source to read.
136
+
137
+ Returns
138
+ -------
139
+ str
140
+ The raw content string.
141
+
142
+ Raises
143
+ ------
144
+ ValueError
145
+ If neither ``raw_content`` nor a valid ``path`` is available.
146
+ FileNotFoundError
147
+ If ``source.path`` points to a non-existent file.
148
+ """
149
+ if source.raw_content is not None:
150
+ return source.raw_content
151
+
152
+ if source.path is not None:
153
+ file_path = Path(source.path)
154
+ if not file_path.exists():
155
+ raise FileNotFoundError(
156
+ f"Source file not found: {source.path!r}"
157
+ )
158
+ return file_path.read_text(encoding="utf-8")
159
+
160
+ raise ValueError(
161
+ "IngestorSource has no raw_content or path — cannot read content."
162
+ )
163
+
164
+ def _make_error_result(
165
+ self, source: IngestorSource, error: str
166
+ ) -> IngestResult:
167
+ """Create a failed :class:`IngestResult` for the given *source*.
168
+
169
+ Parameters
170
+ ----------
171
+ source:
172
+ The source that failed ingestion.
173
+ error:
174
+ Human-readable error message describing the failure.
175
+
176
+ Returns
177
+ -------
178
+ IngestResult
179
+ A result with ``success=False`` and the error in ``errors``.
180
+ """
181
+ return IngestResult(
182
+ success=False,
183
+ source=source,
184
+ errors=[error],
185
+ )
186
+
187
+ def _detect_format(
188
+ self, content: str, source: IngestorSource
189
+ ) -> str | None:
190
+ """Attempt to detect the format of *content*.
191
+
192
+ Performs simple heuristic detection based on well-known keywords and
193
+ file extensions. Concrete plugins may override this for more
194
+ sophisticated detection.
195
+
196
+ Parameters
197
+ ----------
198
+ content:
199
+ The raw content string.
200
+ source:
201
+ The originating source (used to inspect file extension).
202
+
203
+ Returns
204
+ -------
205
+ str | None
206
+ A format identifier string (e.g., ``"openapi"``, ``"postman"``,
207
+ ``"har"``, ``"graphql"``, ``"gherkin"``), or ``None`` if the
208
+ format cannot be determined.
209
+ """
210
+ # Check file extension first
211
+ if source.path is not None:
212
+ name = Path(source.path).name.lower()
213
+ ext = Path(source.path).suffix.lower()
214
+ if name.endswith((".cy.js", ".cy.ts", ".cy.jsx", ".cy.tsx")):
215
+ return "cypress"
216
+ if ext == ".feature":
217
+ return "gherkin"
218
+ if ext == ".graphql" or ext == ".gql":
219
+ return "graphql"
220
+
221
+ # Content-based heuristics (check more specific patterns first)
222
+ stripped = content.strip()
223
+
224
+ # HAR — always JSON with "log" key containing "entries"
225
+ if '"log"' in content and '"entries"' in content and '"request"' in content:
226
+ return "har"
227
+
228
+ # Postman — JSON with "info" and "item" keys
229
+ if '"info"' in content and '"item"' in content and '"postman"' in stripped.lower():
230
+ return "postman"
231
+
232
+ # OpenAPI 3.x
233
+ if "openapi:" in content or '"openapi"' in content:
234
+ return "openapi"
235
+
236
+ # Swagger 2.x
237
+ if "swagger:" in content or '"swagger"' in content:
238
+ return "openapi"
239
+
240
+ # GraphQL SDL
241
+ if stripped.startswith("type ") or "type Query" in content or "schema {" in content:
242
+ return "graphql"
243
+
244
+ # Gherkin
245
+ if stripped.startswith("Feature:") or "Scenario:" in content:
246
+ return "gherkin"
247
+
248
+ # Cypress — cy.visit/cy.get/cy.contains usage
249
+ if "cy.visit(" in content or "cy.get(" in content or "cy.contains(" in content:
250
+ return "cypress"
251
+
252
+ # cURL
253
+ if stripped.startswith("curl ") or "\ncurl " in content:
254
+ return "curl"
255
+
256
+ return None