nat-engine 1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
"""Adaptive testing controller.
|
|
7
|
+
|
|
8
|
+
Maintains a neural-network-based policy that scores candidate test cases
|
|
9
|
+
and decides *which* test to run next (exploitation) vs *generating* a new
|
|
10
|
+
exploratory test (exploration).
|
|
11
|
+
|
|
12
|
+
The exploration–exploitation trade-off is managed with an ε-greedy strategy
|
|
13
|
+
where ε decays over time as the agents gain experience.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import math
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
from mannf.core.neural import NeuralNetwork
|
|
25
|
+
from mannf.core.testing.models import TestCase, TestResult
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Feature dimensionality must match TestCase.feature_vector()
|
|
30
|
+
_FEATURE_DIM = 8
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdaptiveController:
|
|
34
|
+
"""Selects and prioritises test cases using a learned value function.
|
|
35
|
+
|
|
36
|
+
The controller maintains a *value network* V(s) that predicts the
|
|
37
|
+
expected reward (defect-finding potential) of a test case. After each
|
|
38
|
+
test execution, the network is updated with the observed reward via a
|
|
39
|
+
single-sample gradient step (online learning).
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
initial_epsilon:
|
|
44
|
+
Initial exploration probability (0–1). 1 = fully random.
|
|
45
|
+
min_epsilon:
|
|
46
|
+
Minimum exploration probability after decay.
|
|
47
|
+
epsilon_decay:
|
|
48
|
+
Multiplicative decay applied after each test execution.
|
|
49
|
+
learning_rate:
|
|
50
|
+
Step size for the value-network update.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
initial_epsilon: float = 0.9,
|
|
56
|
+
min_epsilon: float = 0.05,
|
|
57
|
+
epsilon_decay: float = 0.995,
|
|
58
|
+
learning_rate: float = 0.005,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.epsilon = initial_epsilon
|
|
61
|
+
self.min_epsilon = min_epsilon
|
|
62
|
+
self.epsilon_decay = epsilon_decay
|
|
63
|
+
|
|
64
|
+
# Value network: maps a test-case feature vector to a scalar reward estimate
|
|
65
|
+
self.value_net = NeuralNetwork(
|
|
66
|
+
layer_sizes=[_FEATURE_DIM, 16, 8, 1],
|
|
67
|
+
hidden_activation="relu",
|
|
68
|
+
learning_rate=learning_rate,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self._step_count = 0
|
|
72
|
+
self._cumulative_reward = 0.0
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
# Selection
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def select(self, candidates: List[TestCase]) -> Optional[TestCase]:
|
|
79
|
+
"""Pick the next test case to execute.
|
|
80
|
+
|
|
81
|
+
With probability *ε* a random candidate is selected (exploration);
|
|
82
|
+
otherwise the candidate with the highest predicted reward is chosen
|
|
83
|
+
(exploitation).
|
|
84
|
+
"""
|
|
85
|
+
if not candidates:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
if np.random.random() < self.epsilon:
|
|
89
|
+
choice = candidates[int(np.random.randint(len(candidates)))]
|
|
90
|
+
logger.debug("AdaptiveController: exploring → %s", choice.id[:8])
|
|
91
|
+
else:
|
|
92
|
+
scores = [
|
|
93
|
+
float(self.value_net.predict(c.feature_vector(_FEATURE_DIM))[0])
|
|
94
|
+
for c in candidates
|
|
95
|
+
]
|
|
96
|
+
best_idx = int(np.argmax(scores))
|
|
97
|
+
choice = candidates[best_idx]
|
|
98
|
+
logger.debug(
|
|
99
|
+
"AdaptiveController: exploiting → %s (score=%.4f)",
|
|
100
|
+
choice.id[:8],
|
|
101
|
+
scores[best_idx],
|
|
102
|
+
)
|
|
103
|
+
return choice
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Learning
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def update(self, test_case: TestCase, result: TestResult) -> float:
|
|
110
|
+
"""Update the value network with the observed reward signal.
|
|
111
|
+
|
|
112
|
+
Returns the MSE training loss for the current step.
|
|
113
|
+
"""
|
|
114
|
+
reward = result.reward_signal()
|
|
115
|
+
self._cumulative_reward += reward
|
|
116
|
+
|
|
117
|
+
x = test_case.feature_vector(_FEATURE_DIM)
|
|
118
|
+
target = np.array([reward])
|
|
119
|
+
loss = self.value_net.train_step(x, target)
|
|
120
|
+
|
|
121
|
+
# Decay exploration rate
|
|
122
|
+
self.epsilon = max(self.min_epsilon, self.epsilon * self.epsilon_decay)
|
|
123
|
+
self._step_count += 1
|
|
124
|
+
|
|
125
|
+
logger.debug(
|
|
126
|
+
"AdaptiveController update: reward=%.3f loss=%.6f ε=%.4f",
|
|
127
|
+
reward,
|
|
128
|
+
loss,
|
|
129
|
+
self.epsilon,
|
|
130
|
+
)
|
|
131
|
+
return loss
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Diagnostics
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def step_count(self) -> int:
|
|
139
|
+
return self._step_count
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def average_reward(self) -> float:
|
|
143
|
+
if self._step_count == 0:
|
|
144
|
+
return 0.0
|
|
145
|
+
return self._cumulative_reward / self._step_count
|
|
146
|
+
|
|
147
|
+
def score(self, test_case: TestCase) -> float:
|
|
148
|
+
"""Return the current predicted value for a test case."""
|
|
149
|
+
return float(self.value_net.predict(test_case.feature_vector(_FEATURE_DIM))[0])
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
"""Core data models for the adaptive testing layer."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum, auto
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestStatus(Enum):
|
|
20
|
+
PENDING = auto()
|
|
21
|
+
RUNNING = auto()
|
|
22
|
+
PASSED = auto()
|
|
23
|
+
FAILED = auto()
|
|
24
|
+
ERROR = auto()
|
|
25
|
+
SKIPPED = auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TestCase:
|
|
30
|
+
"""Represents a single test to be executed against a distributed endpoint.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
target:
|
|
35
|
+
Logical name of the service/endpoint being tested.
|
|
36
|
+
inputs:
|
|
37
|
+
Dictionary of input parameters to send.
|
|
38
|
+
expected_behavior:
|
|
39
|
+
Human-readable description of the expected outcome.
|
|
40
|
+
priority:
|
|
41
|
+
Float in [0, 1]; higher values mean the test should run sooner.
|
|
42
|
+
metadata:
|
|
43
|
+
Arbitrary key-value pairs (e.g. tags, author, generation strategy).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
target: str
|
|
47
|
+
inputs: Dict[str, Any]
|
|
48
|
+
expected_behavior: str
|
|
49
|
+
priority: float = 0.5
|
|
50
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
52
|
+
status: TestStatus = TestStatus.PENDING
|
|
53
|
+
|
|
54
|
+
def feature_vector(self, feature_dim: int = 8) -> "np.ndarray":
|
|
55
|
+
"""Return a fixed-length numeric feature vector used as NN input.
|
|
56
|
+
|
|
57
|
+
The vector encodes: priority, input complexity, id hash projection.
|
|
58
|
+
"""
|
|
59
|
+
import numpy as np
|
|
60
|
+
|
|
61
|
+
vec = np.zeros(feature_dim)
|
|
62
|
+
vec[0] = self.priority
|
|
63
|
+
vec[1] = len(self.inputs) / 20.0 # normalised input arity
|
|
64
|
+
vec[2] = len(self.target) / 50.0 # normalised target length
|
|
65
|
+
# Hash the id into the remaining dimensions for uniqueness
|
|
66
|
+
h = hash(self.id) & 0xFFFFFF
|
|
67
|
+
for i in range(3, feature_dim):
|
|
68
|
+
vec[i] = ((h >> (i * 3)) & 0x7) / 7.0
|
|
69
|
+
return vec
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class TestResult:
|
|
74
|
+
"""The outcome of executing a :class:`TestCase`.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
test_case_id:
|
|
79
|
+
The id of the test case that was executed.
|
|
80
|
+
passed:
|
|
81
|
+
Whether the test produced the expected behaviour.
|
|
82
|
+
actual_output:
|
|
83
|
+
Raw output returned by the system under test.
|
|
84
|
+
execution_time_ms:
|
|
85
|
+
Wall-clock time taken to execute the test.
|
|
86
|
+
error:
|
|
87
|
+
Exception message if an error occurred (``None`` otherwise).
|
|
88
|
+
agent_id:
|
|
89
|
+
ID of the agent that executed/validated the test.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
test_case_id: str
|
|
93
|
+
passed: bool
|
|
94
|
+
actual_output: Any = None
|
|
95
|
+
execution_time_ms: float = 0.0
|
|
96
|
+
error: Optional[str] = None
|
|
97
|
+
agent_id: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def status(self) -> TestStatus:
|
|
101
|
+
if self.error:
|
|
102
|
+
return TestStatus.ERROR
|
|
103
|
+
return TestStatus.PASSED if self.passed else TestStatus.FAILED
|
|
104
|
+
|
|
105
|
+
def reward_signal(self) -> float:
|
|
106
|
+
"""Scalar reward used to update agent neural networks.
|
|
107
|
+
|
|
108
|
+
* +1.0 – test failed (found a real defect; useful information)
|
|
109
|
+
* +0.2 – test errored (interesting, but possibly noisy)
|
|
110
|
+
* -0.1 – test passed (confirms existing behaviour; less informative)
|
|
111
|
+
|
|
112
|
+
The asymmetric reward encourages agents to generate test cases that
|
|
113
|
+
find failures (the primary goal of adaptive testing).
|
|
114
|
+
"""
|
|
115
|
+
if self.error:
|
|
116
|
+
return 0.2
|
|
117
|
+
return 1.0 if not self.passed else -0.1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class TestSuite:
|
|
122
|
+
"""An ordered collection of :class:`TestCase` objects with run statistics."""
|
|
123
|
+
|
|
124
|
+
name: str
|
|
125
|
+
cases: List[TestCase] = field(default_factory=list)
|
|
126
|
+
results: List[TestResult] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
# -- mutators ---------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def add_case(self, case: TestCase) -> None:
|
|
131
|
+
self.cases.append(case)
|
|
132
|
+
|
|
133
|
+
def record_result(self, result: TestResult) -> None:
|
|
134
|
+
self.results.append(result)
|
|
135
|
+
|
|
136
|
+
# -- statistics -------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def total(self) -> int:
|
|
140
|
+
return len(self.results)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def passed(self) -> int:
|
|
144
|
+
return sum(1 for r in self.results if r.passed and not r.error)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def failed(self) -> int:
|
|
148
|
+
return sum(1 for r in self.results if not r.passed and not r.error)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def errored(self) -> int:
|
|
152
|
+
return sum(1 for r in self.results if r.error)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def pass_rate(self) -> float:
|
|
156
|
+
return self.passed / self.total if self.total > 0 else 0.0
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def defect_detection_rate(self) -> float:
|
|
160
|
+
"""Fraction of executions that revealed a defect (failed or errored)."""
|
|
161
|
+
return (self.failed + self.errored) / self.total if self.total > 0 else 0.0
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def average_execution_time_ms(self) -> float:
|
|
165
|
+
if not self.results:
|
|
166
|
+
return 0.0
|
|
167
|
+
return sum(r.execution_time_ms for r in self.results) / len(self.results)
|
|
168
|
+
|
|
169
|
+
def summary(self) -> Dict[str, Any]:
|
|
170
|
+
return {
|
|
171
|
+
"name": self.name,
|
|
172
|
+
"total": self.total,
|
|
173
|
+
"passed": self.passed,
|
|
174
|
+
"failed": self.failed,
|
|
175
|
+
"errored": self.errored,
|
|
176
|
+
"pass_rate": round(self.pass_rate, 4),
|
|
177
|
+
"defect_detection_rate": round(self.defect_detection_rate, 4),
|
|
178
|
+
"avg_execution_time_ms": round(self.average_execution_time_ms, 2),
|
|
179
|
+
}
|
|
@@ -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
|
+
"""Validation sub-package for self-validation and canary testing (Phase 7.4)."""
|
|
7
|
+
|
|
8
|
+
from mannf.core.validation.self_validation_runner import SelfValidationRunner
|
|
9
|
+
|
|
10
|
+
__all__ = ["SelfValidationRunner"]
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
"""Self-validation canary runner (Phase 7.4).
|
|
7
|
+
|
|
8
|
+
:class:`SelfValidationRunner` runs a set of *known-good* and *known-bad*
|
|
9
|
+
canary endpoints against the predictor and compares predicted vs. actual
|
|
10
|
+
outcomes. The accuracy, precision, recall, and F1 metrics are returned in a
|
|
11
|
+
structured report so leadership and auditors can verify detection fidelity.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import math
|
|
18
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SelfValidationRunner:
|
|
24
|
+
"""Compare predicted fault likelihood against known-ground-truth outcomes.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
known_good:
|
|
29
|
+
Sequence of endpoint keys (e.g. ``"/api/health"``) that are expected
|
|
30
|
+
to *pass* (ground truth = non-faulty).
|
|
31
|
+
known_bad:
|
|
32
|
+
Sequence of endpoint keys expected to *fail* (ground truth = faulty).
|
|
33
|
+
threshold:
|
|
34
|
+
Predicted fault-likelihood above which an endpoint is classified as
|
|
35
|
+
*faulty*. Defaults to ``0.5``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
known_good: Sequence[str],
|
|
41
|
+
known_bad: Sequence[str],
|
|
42
|
+
threshold: float = 0.5,
|
|
43
|
+
) -> None:
|
|
44
|
+
if not 0.0 < threshold < 1.0:
|
|
45
|
+
raise ValueError(f"threshold must be in (0.0, 1.0), got {threshold!r}")
|
|
46
|
+
self.known_good: List[str] = list(known_good)
|
|
47
|
+
self.known_bad: List[str] = list(known_bad)
|
|
48
|
+
self.threshold = threshold
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
# Public API
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def run(
|
|
55
|
+
self,
|
|
56
|
+
predicted: Dict[str, float],
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""Evaluate predicted fault likelihoods against ground-truth labels.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
predicted:
|
|
63
|
+
Mapping of ``{endpoint: fault_likelihood}`` (values in [0, 1]).
|
|
64
|
+
Endpoints absent from *predicted* are treated as ``0.0`` (assumed
|
|
65
|
+
safe).
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
dict
|
|
70
|
+
Structured validation report containing:
|
|
71
|
+
|
|
72
|
+
``accuracy``, ``precision``, ``recall``, ``f1`` — overall
|
|
73
|
+
classification metrics (float in [0, 1]).
|
|
74
|
+
|
|
75
|
+
``tp``, ``fp``, ``tn``, ``fn`` — raw confusion-matrix counts.
|
|
76
|
+
|
|
77
|
+
``per_endpoint`` — list of per-endpoint dicts with
|
|
78
|
+
``endpoint``, ``ground_truth``, ``predicted_score``,
|
|
79
|
+
``predicted_label``, ``correct``.
|
|
80
|
+
|
|
81
|
+
``threshold`` — the decision threshold used.
|
|
82
|
+
|
|
83
|
+
``total_canaries`` — total number of canary endpoints evaluated.
|
|
84
|
+
"""
|
|
85
|
+
per_endpoint: List[Dict[str, Any]] = []
|
|
86
|
+
tp = fp = tn = fn = 0
|
|
87
|
+
|
|
88
|
+
all_canaries = [
|
|
89
|
+
(ep, True) for ep in self.known_bad
|
|
90
|
+
] + [
|
|
91
|
+
(ep, False) for ep in self.known_good
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for ep, ground_truth_faulty in all_canaries:
|
|
95
|
+
score = float(predicted.get(ep, 0.0))
|
|
96
|
+
predicted_faulty = score >= self.threshold
|
|
97
|
+
correct = predicted_faulty == ground_truth_faulty
|
|
98
|
+
|
|
99
|
+
if ground_truth_faulty and predicted_faulty:
|
|
100
|
+
tp += 1
|
|
101
|
+
elif not ground_truth_faulty and predicted_faulty:
|
|
102
|
+
fp += 1
|
|
103
|
+
elif not ground_truth_faulty and not predicted_faulty:
|
|
104
|
+
tn += 1
|
|
105
|
+
else:
|
|
106
|
+
fn += 1
|
|
107
|
+
|
|
108
|
+
per_endpoint.append(
|
|
109
|
+
{
|
|
110
|
+
"endpoint": ep,
|
|
111
|
+
"ground_truth": "faulty" if ground_truth_faulty else "healthy",
|
|
112
|
+
"predicted_score": round(score, 4),
|
|
113
|
+
"predicted_label": "faulty" if predicted_faulty else "healthy",
|
|
114
|
+
"correct": correct,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
total = tp + fp + tn + fn
|
|
119
|
+
accuracy = (tp + tn) / total if total > 0 else 0.0
|
|
120
|
+
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
|
121
|
+
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
|
122
|
+
f1 = (
|
|
123
|
+
2 * precision * recall / (precision + recall)
|
|
124
|
+
if (precision + recall) > 0
|
|
125
|
+
else 0.0
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
logger.info(
|
|
129
|
+
"SelfValidationRunner: accuracy=%.3f precision=%.3f recall=%.3f f1=%.3f "
|
|
130
|
+
"(tp=%d fp=%d tn=%d fn=%d threshold=%.2f)",
|
|
131
|
+
accuracy, precision, recall, f1, tp, fp, tn, fn, self.threshold,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"accuracy": round(accuracy, 4),
|
|
136
|
+
"precision": round(precision, 4),
|
|
137
|
+
"recall": round(recall, 4),
|
|
138
|
+
"f1": round(f1, 4),
|
|
139
|
+
"tp": tp,
|
|
140
|
+
"fp": fp,
|
|
141
|
+
"tn": tn,
|
|
142
|
+
"fn": fn,
|
|
143
|
+
"threshold": self.threshold,
|
|
144
|
+
"total_canaries": total,
|
|
145
|
+
"per_endpoint": per_endpoint,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Convenience helper
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def from_risk_report(
|
|
154
|
+
risk_report: List[Dict[str, Any]],
|
|
155
|
+
known_good: Sequence[str],
|
|
156
|
+
known_bad: Sequence[str],
|
|
157
|
+
threshold: float = 0.5,
|
|
158
|
+
) -> "SelfValidationRunner":
|
|
159
|
+
"""Run self-validation directly from a :meth:`RiskScorer.get_risk_report` result.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
risk_report:
|
|
164
|
+
Output of ``RiskScorer.get_risk_report()``.
|
|
165
|
+
known_good:
|
|
166
|
+
Endpoint keys expected to be healthy.
|
|
167
|
+
known_bad:
|
|
168
|
+
Endpoint keys expected to be faulty.
|
|
169
|
+
threshold:
|
|
170
|
+
Decision threshold.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
SelfValidationRunner
|
|
175
|
+
A runner pre-configured with the supplied canary lists.
|
|
176
|
+
"""
|
|
177
|
+
runner = SelfValidationRunner(known_good, known_bad, threshold)
|
|
178
|
+
predicted = {entry["endpoint"]: entry["risk_score"] for entry in risk_report}
|
|
179
|
+
runner._last_report = runner.run(predicted) # type: ignore[attr-defined]
|
|
180
|
+
return runner
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
# Backward-compatible shim — source of truth has moved to mannf.product.dashboard
|
|
7
|
+
from mannf.product.dashboard import * # noqa: F401, F403
|
mannf/dashboard/app.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
# Backward-compatible shim — source of truth has moved to mannf.product.dashboard.app
|
|
7
|
+
import sys
|
|
8
|
+
import mannf.product.dashboard.app as _real
|
|
9
|
+
sys.modules[__name__] = _real
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
# Backward-compatible shim — source of truth has moved to mannf.product.dashboard.models
|
|
7
|
+
import sys
|
|
8
|
+
import mannf.product.dashboard.models as _real
|
|
9
|
+
sys.modules[__name__] = _real
|