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,349 @@
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
+ """DOM snapshot data model and capture utility.
7
+
8
+ This module provides a lightweight, Playwright-agnostic data model for
9
+ representing page DOM state, plus an async helper that populates a
10
+ :class:`DOMSnapshot` from a live Playwright ``Page`` object.
11
+
12
+ Import note: Playwright is **not** imported at module level — it is only
13
+ referenced inside ``capture_dom_snapshot``, so this module can be imported
14
+ and used for pure data-modelling work without Playwright installed.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime, timezone
22
+ from typing import Any
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Interactive HTML tags / selectors that accept user input or navigation.
27
+ _INTERACTIVE_TAGS = frozenset({"button", "input", "select", "textarea"})
28
+
29
+ # JavaScript that extracts element data from the live DOM.
30
+ _DOM_EXTRACTOR_JS = """
31
+ () => {
32
+ const ATTRS = ['name', 'type', 'href', 'placeholder',
33
+ 'aria-label', 'role', 'data-testid'];
34
+ const INTERACTIVE_TAGS = new Set(['button','input','select','textarea']);
35
+ const results = [];
36
+ const allElements = document.querySelectorAll('*');
37
+ allElements.forEach(el => {
38
+ const tag = el.tagName.toLowerCase();
39
+ const elementId = el.id || '';
40
+ const classes = Array.from(el.classList);
41
+ const rawText = (el.innerText || el.textContent || '').trim();
42
+ const text = rawText.length > 200 ? rawText.slice(0, 200) : rawText;
43
+ const attrs = {};
44
+ ATTRS.forEach(a => {
45
+ const v = el.getAttribute(a);
46
+ if (v !== null) attrs[a] = v;
47
+ });
48
+ const isInteractive = INTERACTIVE_TAGS.has(tag) ||
49
+ (tag === 'a' && el.hasAttribute('href'));
50
+ let bbox = null;
51
+ try {
52
+ const r = el.getBoundingClientRect();
53
+ if (r.width > 0 || r.height > 0) {
54
+ bbox = {x: r.x, y: r.y, width: r.width, height: r.height};
55
+ }
56
+ } catch(_) {}
57
+ results.push({tag, elementId, classes, text, attrs, isInteractive, bbox});
58
+ });
59
+ return results;
60
+ }
61
+ """
62
+
63
+ # JavaScript that extracts <form> structures.
64
+ _FORM_EXTRACTOR_JS = """
65
+ () => {
66
+ const forms = [];
67
+ document.querySelectorAll('form').forEach(form => {
68
+ const fields = [];
69
+ form.querySelectorAll('input, select, textarea').forEach(el => {
70
+ fields.push({
71
+ tag: el.tagName.toLowerCase(),
72
+ name: el.getAttribute('name') || '',
73
+ type: el.getAttribute('type') || '',
74
+ placeholder: el.getAttribute('placeholder') || '',
75
+ });
76
+ });
77
+ forms.push({
78
+ action: form.getAttribute('action') || '',
79
+ method: (form.getAttribute('method') || 'get').toLowerCase(),
80
+ fields,
81
+ });
82
+ });
83
+ return forms;
84
+ }
85
+ """
86
+
87
+
88
+ # JavaScript that extracts <form> structures with enhanced field metadata
89
+ # (required flag, associated label text, HTML5 validation pattern, and
90
+ # per-form submit button selector). Used by WebCrawlerAgent.
91
+ _ENHANCED_FORM_EXTRACTOR_JS = """
92
+ () => {
93
+ function buildSelector(el) {
94
+ if (el.id) return '#' + el.id;
95
+ const testid = el.getAttribute('data-testid');
96
+ if (testid) return '[data-testid="' + testid + '"]';
97
+ const name = el.getAttribute('name');
98
+ if (name) return el.tagName.toLowerCase() + '[name="' + name + '"]';
99
+ const classes = Array.from(el.classList);
100
+ if (classes.length) return el.tagName.toLowerCase() + '.' + classes.join('.');
101
+ return el.tagName.toLowerCase();
102
+ }
103
+
104
+ function findLabel(el) {
105
+ // 1. <label for="id">
106
+ if (el.id) {
107
+ const lbl = document.querySelector('label[for="' + el.id + '"]');
108
+ if (lbl) return (lbl.innerText || lbl.textContent || '').trim();
109
+ }
110
+ // 2. Ancestor <label>
111
+ let node = el.parentElement;
112
+ while (node) {
113
+ if (node.tagName && node.tagName.toLowerCase() === 'label') {
114
+ return (node.innerText || node.textContent || '').trim();
115
+ }
116
+ node = node.parentElement;
117
+ }
118
+ // 3. aria-label attribute
119
+ return el.getAttribute('aria-label') || '';
120
+ }
121
+
122
+ const forms = [];
123
+ document.querySelectorAll('form').forEach(form => {
124
+ const fields = [];
125
+ form.querySelectorAll('input, select, textarea').forEach(el => {
126
+ fields.push({
127
+ tag: el.tagName.toLowerCase(),
128
+ name: el.getAttribute('name') || '',
129
+ type: (el.getAttribute('type') || el.tagName.toLowerCase() === 'select'
130
+ ? (el.tagName.toLowerCase() === 'select' ? 'select'
131
+ : (el.tagName.toLowerCase() === 'textarea' ? 'textarea'
132
+ : (el.getAttribute('type') || 'text')))
133
+ : 'text'),
134
+ placeholder: el.getAttribute('placeholder') || '',
135
+ required: !!(el.required || el.hasAttribute('required')),
136
+ label: findLabel(el),
137
+ pattern: el.getAttribute('pattern') || '',
138
+ selector: buildSelector(el),
139
+ });
140
+ });
141
+
142
+ // Identify submit selector: prefer button[type=submit], then
143
+ // input[type=submit], then the last button in the form.
144
+ let submitSel = '';
145
+ const submitBtn = form.querySelector('button[type="submit"]');
146
+ if (submitBtn) {
147
+ submitSel = buildSelector(submitBtn);
148
+ } else {
149
+ const submitInput = form.querySelector('input[type="submit"]');
150
+ if (submitInput) {
151
+ submitSel = buildSelector(submitInput);
152
+ } else {
153
+ const buttons = form.querySelectorAll('button');
154
+ if (buttons.length) {
155
+ submitSel = buildSelector(buttons[buttons.length - 1]);
156
+ }
157
+ }
158
+ }
159
+
160
+ forms.push({
161
+ action: form.getAttribute('action') || '',
162
+ method: (form.getAttribute('method') || 'get').toUpperCase(),
163
+ fields,
164
+ submit_selector: submitSel,
165
+ });
166
+ });
167
+ return forms;
168
+ }
169
+ """
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Helper
174
+ # ---------------------------------------------------------------------------
175
+
176
+ def build_selector(
177
+ tag: str,
178
+ element_id: str,
179
+ classes: list[str],
180
+ attributes: dict[str, str],
181
+ ) -> str:
182
+ """Generate a CSS selector with priority: id > data-testid > name > class > tag.
183
+
184
+ Parameters
185
+ ----------
186
+ tag:
187
+ HTML element tag name (lower-cased).
188
+ element_id:
189
+ Value of the element's ``id`` attribute (empty string if absent).
190
+ classes:
191
+ List of CSS class names on the element.
192
+ attributes:
193
+ Dict of key HTML attributes (name, type, href, placeholder, etc.).
194
+
195
+ Returns
196
+ -------
197
+ str
198
+ A CSS selector string that can be used to locate the element.
199
+ """
200
+ if element_id:
201
+ return f"#{element_id}"
202
+
203
+ data_testid = attributes.get("data-testid", "")
204
+ if data_testid:
205
+ return f'[data-testid="{data_testid}"]'
206
+
207
+ name = attributes.get("name", "")
208
+ if name:
209
+ return f'{tag}[name="{name}"]'
210
+
211
+ if classes:
212
+ class_part = ".".join(classes)
213
+ return f"{tag}.{class_part}"
214
+
215
+ return tag
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Data model
220
+ # ---------------------------------------------------------------------------
221
+
222
+ @dataclass
223
+ class DOMElement:
224
+ """Represents a single DOM element extracted from a browser page.
225
+
226
+ Parameters
227
+ ----------
228
+ tag:
229
+ Lower-cased HTML tag name (e.g. ``"input"``, ``"button"``).
230
+ element_id:
231
+ Value of the ``id`` attribute; empty string if absent.
232
+ classes:
233
+ List of CSS class names present on the element.
234
+ text:
235
+ Visible text content, truncated to 200 characters.
236
+ attributes:
237
+ Dict of key HTML attributes (name, type, href, placeholder,
238
+ aria-label, role, data-testid).
239
+ selector:
240
+ A generated CSS selector for this element.
241
+ is_interactive:
242
+ ``True`` if the element is clickable or fillable.
243
+ bounding_box:
244
+ ``{x, y, width, height}`` in pixels, or ``None`` if unavailable.
245
+ """
246
+
247
+ tag: str
248
+ element_id: str
249
+ classes: list[str]
250
+ text: str
251
+ attributes: dict[str, str]
252
+ selector: str
253
+ is_interactive: bool
254
+ bounding_box: dict[str, float] | None = None
255
+
256
+
257
+ @dataclass
258
+ class DOMSnapshot:
259
+ """Represents a full-page DOM snapshot captured from a browser.
260
+
261
+ Parameters
262
+ ----------
263
+ url:
264
+ The page URL at capture time.
265
+ title:
266
+ The page ``<title>`` text.
267
+ timestamp:
268
+ ISO 8601 UTC timestamp of when the snapshot was taken.
269
+ elements:
270
+ All DOM elements discovered on the page.
271
+ forms:
272
+ Extracted form structures (action, method, fields).
273
+ """
274
+
275
+ url: str
276
+ title: str
277
+ timestamp: str
278
+ elements: list[DOMElement]
279
+ forms: list[dict[str, Any]] = field(default_factory=list)
280
+
281
+ @property
282
+ def interactive_elements(self) -> list[DOMElement]:
283
+ """Return only the elements marked as interactive."""
284
+ return [el for el in self.elements if el.is_interactive]
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # Capture
289
+ # ---------------------------------------------------------------------------
290
+
291
+ async def capture_dom_snapshot(page: Any) -> DOMSnapshot:
292
+ """Capture a :class:`DOMSnapshot` from a live Playwright ``Page``.
293
+
294
+ Parameters
295
+ ----------
296
+ page:
297
+ A Playwright ``Page`` object (typed as ``Any`` to avoid a hard
298
+ import-time dependency on ``playwright``).
299
+
300
+ Returns
301
+ -------
302
+ DOMSnapshot
303
+ The full DOM state of the page at call time.
304
+ """
305
+ url: str = page.url
306
+ title: str = await page.title()
307
+ timestamp: str = datetime.now(timezone.utc).isoformat()
308
+
309
+ raw_elements: list[dict[str, Any]] = await page.evaluate(_DOM_EXTRACTOR_JS)
310
+
311
+ elements: list[DOMElement] = []
312
+ for raw in raw_elements:
313
+ tag = raw.get("tag", "")
314
+ element_id = raw.get("elementId", "")
315
+ classes = raw.get("classes", [])
316
+ text = raw.get("text", "")
317
+ attrs = raw.get("attrs", {})
318
+ is_interactive = raw.get("isInteractive", False)
319
+ bbox = raw.get("bbox")
320
+
321
+ selector = build_selector(tag, element_id, classes, attrs)
322
+
323
+ elements.append(
324
+ DOMElement(
325
+ tag=tag,
326
+ element_id=element_id,
327
+ classes=classes,
328
+ text=text,
329
+ attributes=attrs,
330
+ selector=selector,
331
+ is_interactive=is_interactive,
332
+ bounding_box=bbox,
333
+ )
334
+ )
335
+
336
+ forms: list[dict[str, Any]] = await page.evaluate(_FORM_EXTRACTOR_JS)
337
+
338
+ logger.debug(
339
+ "capture_dom_snapshot: url=%r elements=%d forms=%d",
340
+ url, len(elements), len(forms),
341
+ )
342
+
343
+ return DOMSnapshot(
344
+ url=url,
345
+ title=title,
346
+ timestamp=timestamp,
347
+ elements=elements,
348
+ forms=forms,
349
+ )