igel-qe-core 1.0.7 → 1.0.9
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.
- package/dist/cli/index.js +24 -7
- package/dist/mcp/server.js +23 -8
- package/package.json +1 -1
- package/src/agents/__pycache__/automation_design_agent.cpython-313.pyc +0 -0
- package/src/agents/__pycache__/confidence_agent.cpython-313.pyc +0 -0
- package/src/agents/__pycache__/execution_validation_agent.cpython-313.pyc +0 -0
- package/src/agents/__pycache__/workflow_engine.cpython-313.pyc +0 -0
- package/src/agents/automation_design_agent.py +39 -6
- package/src/agents/confidence_agent.py +157 -51
- package/src/agents/execution_validation_agent.py +176 -58
- package/src/agents/workflow_engine.py +18 -13
- package/src/cli/__pycache__/workflow_cli.cpython-313.pyc +0 -0
- package/src/cli/index.ts +24 -7
- package/src/cli/workflow_cli.py +11 -1
- package/src/deploy/requirements.txt +1 -0
- package/src/mcp/server.ts +22 -8
- package/src/models/__pycache__/test_requirement.cpython-313.pyc +0 -0
- package/src/models/test_requirement.py +13 -3
package/dist/cli/index.js
CHANGED
|
@@ -15,9 +15,26 @@ program
|
|
|
15
15
|
.description("IGEL QE Developer Experience CLI")
|
|
16
16
|
.version("1.0.0");
|
|
17
17
|
// ── helpers ────────────────────────────────────────────────────────────────────
|
|
18
|
+
function getSystemPython() {
|
|
19
|
+
for (const cmd of ["python3", "python", "py"]) {
|
|
20
|
+
try {
|
|
21
|
+
const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
22
|
+
if (res.status === 0 || res.status === null) {
|
|
23
|
+
return cmd;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Continue
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return "python3";
|
|
31
|
+
}
|
|
18
32
|
function getPythonBin() {
|
|
19
|
-
const
|
|
20
|
-
|
|
33
|
+
const isWin = process.platform === "win32";
|
|
34
|
+
const venv = isWin
|
|
35
|
+
? join(PROJECT_ROOT, ".venv", "Scripts", "python.exe")
|
|
36
|
+
: join(PROJECT_ROOT, ".venv", "bin", "python");
|
|
37
|
+
return existsSync(venv) ? venv : getSystemPython();
|
|
21
38
|
}
|
|
22
39
|
function resolveRequirementsPath() {
|
|
23
40
|
const primary = join(PROJECT_ROOT, "requirements.txt");
|
|
@@ -196,11 +213,11 @@ function listFrameworkContextFiles(workspaceRoot) {
|
|
|
196
213
|
return Array.from(new Set(found)).sort();
|
|
197
214
|
}
|
|
198
215
|
function isCopilotCliAvailable() {
|
|
199
|
-
const check = spawnSync("copilot", ["--version"], { encoding: "utf-8" });
|
|
216
|
+
const check = spawnSync("copilot", ["--version"], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
200
217
|
return check.status === 0;
|
|
201
218
|
}
|
|
202
219
|
function isCopilotAuthenticated(workspaceRoot) {
|
|
203
|
-
const probe = spawnSync("copilot", ["-C", workspaceRoot, "-p", "Reply with exactly OK.", "--allow-all-tools", "--no-color", "-s"], { encoding: "utf-8" });
|
|
220
|
+
const probe = spawnSync("copilot", ["-C", workspaceRoot, "-p", "Reply with exactly OK.", "--allow-all-tools", "--no-color", "-s"], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
204
221
|
return probe.status === 0;
|
|
205
222
|
}
|
|
206
223
|
function ensureCopilotLogin(workspaceRoot) {
|
|
@@ -210,7 +227,7 @@ function ensureCopilotLogin(workspaceRoot) {
|
|
|
210
227
|
return true;
|
|
211
228
|
console.log(chalk.yellow(" ⚠ GitHub Copilot CLI is not authenticated."));
|
|
212
229
|
console.log(chalk.yellow(" → Please complete Copilot login to continue context enrichment."));
|
|
213
|
-
const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8" });
|
|
230
|
+
const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8", shell: process.platform === "win32" });
|
|
214
231
|
if (login.status !== 0)
|
|
215
232
|
return false;
|
|
216
233
|
return isCopilotAuthenticated(workspaceRoot);
|
|
@@ -237,7 +254,7 @@ function enrichWithCopilotCli(workspaceRoot, contextFiles) {
|
|
|
237
254
|
"--allow-all-paths",
|
|
238
255
|
"--no-color",
|
|
239
256
|
"-s",
|
|
240
|
-
], { encoding: "utf-8" });
|
|
257
|
+
], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
241
258
|
if (result.status === 0) {
|
|
242
259
|
updated += 1;
|
|
243
260
|
}
|
|
@@ -345,7 +362,7 @@ program
|
|
|
345
362
|
if (!existsSync(venvPath)) {
|
|
346
363
|
console.log(chalk.yellow("1. Creating virtual environment…"));
|
|
347
364
|
try {
|
|
348
|
-
execSync(
|
|
365
|
+
execSync(`${getSystemPython()} -m venv .venv`, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
349
366
|
console.log(chalk.green(" ✓ venv created"));
|
|
350
367
|
}
|
|
351
368
|
catch (e) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import { spawn } from "child_process";
|
|
6
|
+
import { spawn, spawnSync } from "child_process";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import fs from "fs";
|
|
@@ -11,18 +11,33 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
// Project root: dist/mcp/server.js -> ../../ = project root
|
|
12
12
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
13
|
// Resolve the correct Python executable:
|
|
14
|
-
// 1. Check for a local .venv
|
|
15
|
-
// 2. Fall back to system python3 / python
|
|
14
|
+
// 1. Check for a local .venv (Linux or Windows path layouts)
|
|
15
|
+
// 2. Fall back to system python3 / python / py
|
|
16
16
|
function resolvePythonExecutable() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const isWin = process.platform === 'win32';
|
|
18
|
+
const candidates = isWin
|
|
19
|
+
? [
|
|
20
|
+
path.join(PROJECT_ROOT, '.venv', 'Scripts', 'python.exe'),
|
|
21
|
+
path.join(PROJECT_ROOT, '.venv', 'Scripts', 'python'),
|
|
22
|
+
]
|
|
23
|
+
: [
|
|
24
|
+
path.join(PROJECT_ROOT, '.venv', 'bin', 'python'),
|
|
25
|
+
path.join(PROJECT_ROOT, '.venv', 'bin', 'python3'),
|
|
26
|
+
];
|
|
21
27
|
for (const c of candidates) {
|
|
22
28
|
if (fs.existsSync(c))
|
|
23
29
|
return c;
|
|
24
30
|
}
|
|
25
|
-
// Check system python3 / python
|
|
31
|
+
// Check system python3 / python / py
|
|
32
|
+
for (const cmd of ['python3', 'python', 'py']) {
|
|
33
|
+
try {
|
|
34
|
+
const res = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
|
|
35
|
+
if (res.status === 0 || res.status === null) {
|
|
36
|
+
return cmd;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
26
41
|
return 'python3';
|
|
27
42
|
}
|
|
28
43
|
const server = new Server({
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -15,6 +15,11 @@ class AutomationPlan(BaseModel):
|
|
|
15
15
|
page_objects: List[str] = Field(default_factory=list)
|
|
16
16
|
fixtures: List[str] = Field(default_factory=list)
|
|
17
17
|
utilities: List[str] = Field(default_factory=list)
|
|
18
|
+
# Reusability Metric:
|
|
19
|
+
# (No. of reusable components generated / Total components generated) × 100 → normalised 0-1
|
|
20
|
+
reusability_score: float = 0.0
|
|
21
|
+
registry_sourced_count: int = 0
|
|
22
|
+
total_components_count: int = 0
|
|
18
23
|
|
|
19
24
|
_SYSTEM_PROMPT = load_prompt(
|
|
20
25
|
"automation_design.md",
|
|
@@ -106,15 +111,43 @@ def design_automation_plan(test_cases: List[TestCaseWithConfidence]) -> Automati
|
|
|
106
111
|
|
|
107
112
|
content = response.choices[0].message.content or "{}"
|
|
108
113
|
data = json.loads(content)
|
|
109
|
-
|
|
114
|
+
|
|
115
|
+
pos = data.get("page_objects", [])
|
|
116
|
+
fixtures_list = data.get("fixtures", [])
|
|
117
|
+
utils_list = data.get("utilities", [])
|
|
118
|
+
|
|
119
|
+
# ── Reusability Metric ────────────────────────────────────────────────
|
|
120
|
+
# Formula: (No. of reusable components / Total components) × 100 → 0-1
|
|
121
|
+
# A component is 'reusable' if it was drawn from the Asset Registry (available_assets).
|
|
122
|
+
all_registry_names: set[str] = set()
|
|
123
|
+
for asset_list in available_assets.values():
|
|
124
|
+
for entry in asset_list:
|
|
125
|
+
# Entry format: "name (description...)"
|
|
126
|
+
name_part = entry.split("(")[0].strip()
|
|
127
|
+
all_registry_names.add(name_part.lower())
|
|
128
|
+
|
|
129
|
+
all_generated = pos + fixtures_list + utils_list
|
|
130
|
+
registry_sourced = sum(
|
|
131
|
+
1 for c in all_generated
|
|
132
|
+
if c.split("(")[0].strip().lower() in all_registry_names
|
|
133
|
+
)
|
|
134
|
+
total_components = len(all_generated)
|
|
135
|
+
reusability = (registry_sourced / total_components) if total_components > 0 else 0.0
|
|
136
|
+
|
|
110
137
|
plan = AutomationPlan(
|
|
111
|
-
page_objects=
|
|
112
|
-
fixtures=
|
|
113
|
-
utilities=
|
|
138
|
+
page_objects=pos,
|
|
139
|
+
fixtures=fixtures_list,
|
|
140
|
+
utilities=utils_list,
|
|
141
|
+
reusability_score=round(reusability, 4),
|
|
142
|
+
registry_sourced_count=registry_sourced,
|
|
143
|
+
total_components_count=total_components,
|
|
144
|
+
)
|
|
145
|
+
logger.info(
|
|
146
|
+
"Designed Automation Plan with %d POs, %d fixtures | reusability=%.2f",
|
|
147
|
+
len(plan.page_objects), len(plan.fixtures), reusability,
|
|
114
148
|
)
|
|
115
|
-
logger.info(f"Designed Automation Plan with {len(plan.page_objects)} POs, {len(plan.fixtures)} fixtures")
|
|
116
149
|
return plan
|
|
117
|
-
|
|
150
|
+
|
|
118
151
|
except Exception as e:
|
|
119
152
|
logger.error(f"Failed to design automation plan: {e}")
|
|
120
153
|
return AutomationPlan()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from src.models.test_requirement import (
|
|
3
|
-
GeneratedTestCase,
|
|
3
|
+
GeneratedTestCase,
|
|
4
4
|
ScenarioWithRules,
|
|
5
5
|
ConfidenceMetrics,
|
|
6
|
-
TestCaseWithConfidence
|
|
6
|
+
TestCaseWithConfidence,
|
|
7
7
|
)
|
|
8
8
|
from src.core.context_engine import SourceReport
|
|
9
9
|
from src.core.evidence_resolver import ResolvedContext
|
|
@@ -12,21 +12,116 @@ from src.agents.validation_agent import validate_structural
|
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
# ── Parameter Calculation Helpers ─────────────────────────────────────────────
|
|
17
|
+
|
|
15
18
|
def evaluate_completeness(tc: GeneratedTestCase) -> float:
|
|
16
|
-
"""Evaluate completeness of steps
|
|
19
|
+
"""Evaluate structural completeness of steps (action + data + expected_result).
|
|
20
|
+
|
|
21
|
+
Used by Structural Quality dimension.
|
|
22
|
+
Formula proxy: each complete step scores 1.0; partial steps score proportionally.
|
|
23
|
+
"""
|
|
17
24
|
if not tc.steps:
|
|
18
25
|
return 0.0
|
|
19
|
-
|
|
20
26
|
score = 0.0
|
|
21
27
|
for step in tc.steps:
|
|
22
28
|
step_score = 0.0
|
|
23
|
-
if step.action:
|
|
24
|
-
|
|
25
|
-
if step.
|
|
29
|
+
if step.action:
|
|
30
|
+
step_score += 0.4
|
|
31
|
+
if step.data:
|
|
32
|
+
step_score += 0.2
|
|
33
|
+
if step.expected_result:
|
|
34
|
+
step_score += 0.4
|
|
26
35
|
score += step_score
|
|
27
|
-
|
|
28
36
|
return score / len(tc.steps)
|
|
29
37
|
|
|
38
|
+
|
|
39
|
+
def compute_contextual_relevance(tc: GeneratedTestCase, scenario: ScenarioWithRules) -> float:
|
|
40
|
+
"""Contextual Relevance — Degree to which generated test cases align with requirement/business context.
|
|
41
|
+
|
|
42
|
+
Formula: (No. of test steps mapped to requirement context / Total generated test steps) × 100 → normalised 0-1.
|
|
43
|
+
Mapping heuristic: a step is considered 'mapped' if its action is non-empty and can be linked back
|
|
44
|
+
to at least one of the scenario's business rules (via text intersection).
|
|
45
|
+
"""
|
|
46
|
+
if not tc.steps:
|
|
47
|
+
return 0.0
|
|
48
|
+
rule_keywords: set[str] = set()
|
|
49
|
+
for rule in scenario.business_rules:
|
|
50
|
+
for word in rule.description.lower().split():
|
|
51
|
+
if len(word) > 4:
|
|
52
|
+
rule_keywords.add(word)
|
|
53
|
+
|
|
54
|
+
mapped = 0
|
|
55
|
+
for step in tc.steps:
|
|
56
|
+
action_words = set(step.action.lower().split())
|
|
57
|
+
if rule_keywords and action_words & rule_keywords:
|
|
58
|
+
mapped += 1
|
|
59
|
+
elif step.action:
|
|
60
|
+
# Fallback: if no keyword overlap but step exists, give partial credit
|
|
61
|
+
mapped += 0.5
|
|
62
|
+
|
|
63
|
+
return min(1.0, (mapped / len(tc.steps)))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def compute_hallucination_control(tc: GeneratedTestCase, resolved_context: ResolvedContext) -> float:
|
|
67
|
+
"""Hallucination — Percentage of generated artifacts NOT containing non-existent entities.
|
|
68
|
+
|
|
69
|
+
Formula: (No. of non-hallucinated entities / Total generated entities) × 100 → normalised 0-1.
|
|
70
|
+
Detection heuristic: presence of unresolved source conflicts in resolved_context signals that the
|
|
71
|
+
AI may have invented information. Zero conflicts = no hallucination detected.
|
|
72
|
+
"""
|
|
73
|
+
conflict_count = len(resolved_context.conflicting_sources)
|
|
74
|
+
# Each conflict reduces confidence. Cap at 5 conflicts = 0 score.
|
|
75
|
+
return max(0.0, 1.0 - (conflict_count * 0.2))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compute_bias_fairness_control(tc: GeneratedTestCase, scenario: ScenarioWithRules) -> float:
|
|
79
|
+
"""Bias/Fairness Control — Ensures generation covers all personas, locales, negative and edge cases.
|
|
80
|
+
|
|
81
|
+
Formula: (No. of required personas/scenarios covered / Total identified) × 100 → normalised 0-1.
|
|
82
|
+
Coverage heuristic: checks if positive, negative and edge rule types are present in the business rules,
|
|
83
|
+
and if the specific TC covers one of these.
|
|
84
|
+
"""
|
|
85
|
+
rule_types_present = {rule.rule_type for rule in scenario.business_rules}
|
|
86
|
+
total_expected = {"positive", "negative", "edge"}
|
|
87
|
+
covered = rule_types_present & total_expected
|
|
88
|
+
coverage_ratio = len(covered) / len(total_expected)
|
|
89
|
+
|
|
90
|
+
# Bonus: TC itself covers its declared type
|
|
91
|
+
if tc.test_type in covered:
|
|
92
|
+
coverage_ratio = min(1.0, coverage_ratio + 0.15)
|
|
93
|
+
|
|
94
|
+
return round(coverage_ratio, 4)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def compute_business_rule_coverage(tc: GeneratedTestCase, scenario: ScenarioWithRules) -> float:
|
|
98
|
+
"""Business Rule Coverage — Checks how well generated steps address the identified business rules.
|
|
99
|
+
|
|
100
|
+
Formula: (No. of business rules covered by test steps / Total business rules) × 100 → normalised 0-1.
|
|
101
|
+
"""
|
|
102
|
+
if not scenario.business_rules:
|
|
103
|
+
return 1.0
|
|
104
|
+
return min(1.0, len(tc.steps) / len(scenario.business_rules))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def compute_requirement_clarity(scenario: ScenarioWithRules, source_report: SourceReport) -> float:
|
|
108
|
+
"""Requirement Clarity — How well the upstream requirement was understood.
|
|
109
|
+
|
|
110
|
+
Formula: Determined by presence of structured business rules AND multi-source corroboration.
|
|
111
|
+
"""
|
|
112
|
+
has_rules = 1.0 if scenario.business_rules else 0.5
|
|
113
|
+
sources_found = sum([
|
|
114
|
+
source_report.jira_found,
|
|
115
|
+
source_report.confluence_found,
|
|
116
|
+
source_report.postgres_kb_found,
|
|
117
|
+
source_report.igel_os12_docs_found,
|
|
118
|
+
])
|
|
119
|
+
source_bonus = min(0.5, sources_found / 4 * 0.5)
|
|
120
|
+
return min(1.0, has_rules * 0.5 + source_bonus)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── Main Confidence Computation ───────────────────────────────────────────────
|
|
124
|
+
|
|
30
125
|
def compute_two_level_confidence(
|
|
31
126
|
tc: GeneratedTestCase,
|
|
32
127
|
scenario: ScenarioWithRules,
|
|
@@ -34,81 +129,92 @@ def compute_two_level_confidence(
|
|
|
34
129
|
source_report: SourceReport,
|
|
35
130
|
policy: PolicyConfig,
|
|
36
131
|
) -> TestCaseWithConfidence:
|
|
37
|
-
"""
|
|
38
|
-
|
|
132
|
+
"""Compute the full confidence score using the 8-parameter framework.
|
|
133
|
+
|
|
134
|
+
Pre-execution parameters (set here):
|
|
135
|
+
1. Contextual Relevance – steps mapped to req context
|
|
136
|
+
2. Hallucination Control – grounded vs invented entities
|
|
137
|
+
3. Bias/Fairness Control – persona/scenario type coverage
|
|
138
|
+
4. Business Rule Coverage – rules addressed by steps
|
|
139
|
+
5. Requirement Clarity – clarity of upstream requirement
|
|
140
|
+
6. Structural Quality – step completeness (action+data+expected)
|
|
141
|
+
|
|
142
|
+
Post-execution parameters (set by ExecutionValidationAgent / HealingAgent):
|
|
143
|
+
7. Maintainability – complexity + duplication (in ExecutionMetrics)
|
|
144
|
+
8. First-Pass Success – runtime result on first run (in ExecutionMetrics)
|
|
145
|
+
9. Reusability – registry-sourced vs new components (in ExecutionMetrics)
|
|
146
|
+
10. Flakiness – historical inconsistency rate (in ExecutionMetrics)
|
|
39
147
|
"""
|
|
40
148
|
dims = policy.confidence_dimensions
|
|
41
149
|
|
|
42
|
-
#
|
|
150
|
+
# ── Pre-Execution Dimensions ──────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
# 1. Context Strength (informational, not weighted)
|
|
43
153
|
sources_found = sum([
|
|
44
|
-
source_report.jira_found,
|
|
45
|
-
source_report.
|
|
154
|
+
source_report.jira_found,
|
|
155
|
+
source_report.confluence_found,
|
|
156
|
+
source_report.postgres_kb_found,
|
|
157
|
+
source_report.igel_os12_docs_found,
|
|
46
158
|
])
|
|
47
159
|
context_strength = min(1.0, sources_found / 4.0)
|
|
48
160
|
|
|
49
|
-
# 2.
|
|
50
|
-
|
|
51
|
-
# Since our schema for ScenarioWithRules just has scenario and business_rules,
|
|
52
|
-
# we check for presence of multiple business_rules as a proxy for clarity.
|
|
53
|
-
requirement_clarity = 1.0 if scenario.business_rules else 0.5
|
|
161
|
+
# 2. Contextual Relevance
|
|
162
|
+
contextual_relevance = compute_contextual_relevance(tc, scenario)
|
|
54
163
|
|
|
55
|
-
# 3.
|
|
56
|
-
|
|
57
|
-
conflict_count = len(resolved_context.conflicting_sources)
|
|
58
|
-
evidence_quality = 1.0 if conflict_count == 0 else max(0.0, 1.0 - (0.3 * conflict_count))
|
|
164
|
+
# 3. Hallucination Control (replaces evidence_quality)
|
|
165
|
+
hallucination_control = compute_hallucination_control(tc, resolved_context)
|
|
59
166
|
|
|
60
|
-
# 4.
|
|
61
|
-
|
|
62
|
-
rule_coverage = 1.0 if len(tc.steps) >= len(scenario.business_rules) else 0.5
|
|
167
|
+
# 4. Bias/Fairness Control (replaces scenario_coverage)
|
|
168
|
+
bias_fairness_control = compute_bias_fairness_control(tc, scenario)
|
|
63
169
|
|
|
64
|
-
# 5.
|
|
65
|
-
|
|
66
|
-
traceability = 1.0 if "S" in tc.tc_reference and tc.business_rule else 0.0
|
|
170
|
+
# 5. Business Rule Coverage
|
|
171
|
+
business_rule_coverage = compute_business_rule_coverage(tc, scenario)
|
|
67
172
|
|
|
68
|
-
# 6.
|
|
69
|
-
|
|
70
|
-
# Checking the single TC test_type
|
|
71
|
-
scenario_score = 1.0 if tc.test_type in ["positive", "negative", "edge"] else 0.5
|
|
173
|
+
# 6. Requirement Clarity
|
|
174
|
+
requirement_clarity = compute_requirement_clarity(scenario, source_report)
|
|
72
175
|
|
|
73
|
-
# 7.
|
|
74
|
-
|
|
75
|
-
struct_ok, struct_errs = validate_structural(tc, scenario)
|
|
176
|
+
# 7. Structural Quality
|
|
177
|
+
struct_ok, _ = validate_structural(tc, scenario)
|
|
76
178
|
structural_quality = evaluate_completeness(tc) if struct_ok else 0.0
|
|
77
179
|
|
|
78
|
-
# Weighted
|
|
180
|
+
# ── Weighted Final Score ──────────────────────────────────────────────────
|
|
181
|
+
# Weights pulled from policy dimensions; field names updated to match new params
|
|
79
182
|
total = (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
structural_quality
|
|
183
|
+
contextual_relevance * getattr(dims, "contextual_relevance", 0.25) +
|
|
184
|
+
hallucination_control * getattr(dims, "hallucination_control", 0.20) +
|
|
185
|
+
bias_fairness_control * getattr(dims, "bias_fairness_control", 0.15) +
|
|
186
|
+
business_rule_coverage * getattr(dims, "business_rule_coverage", 0.15) +
|
|
187
|
+
requirement_clarity * getattr(dims, "requirement_clarity", 0.15) +
|
|
188
|
+
structural_quality * getattr(dims, "structural_quality", 0.10)
|
|
86
189
|
)
|
|
87
190
|
|
|
88
191
|
metrics = ConfidenceMetrics(
|
|
89
192
|
context_strength=context_strength,
|
|
90
193
|
requirement_clarity=requirement_clarity,
|
|
91
|
-
|
|
92
|
-
business_rule_coverage=
|
|
93
|
-
|
|
94
|
-
|
|
194
|
+
hallucination_control=hallucination_control,
|
|
195
|
+
business_rule_coverage=business_rule_coverage,
|
|
196
|
+
contextual_relevance=contextual_relevance,
|
|
197
|
+
bias_fairness_control=bias_fairness_control,
|
|
95
198
|
structural_quality=structural_quality,
|
|
96
|
-
final_confidence=total,
|
|
199
|
+
final_confidence=round(total, 4),
|
|
97
200
|
)
|
|
98
201
|
|
|
99
202
|
tc.confidence_score = total
|
|
100
|
-
tc.confidence_metrics = metrics.model_dump() if hasattr(metrics,
|
|
203
|
+
tc.confidence_metrics = metrics.model_dump() if hasattr(metrics, "model_dump") else metrics.dict()
|
|
101
204
|
|
|
102
205
|
bands = policy.confidence_bands
|
|
103
206
|
is_approved = total >= bands.auto_approved
|
|
104
207
|
|
|
105
208
|
if not is_approved:
|
|
106
|
-
logger.info(
|
|
209
|
+
logger.info(
|
|
210
|
+
"Test case %s needs review (score: %.2f | contextual_relevance=%.2f, hallucination=%.2f, bias=%.2f)",
|
|
211
|
+
tc.tc_reference, total, contextual_relevance, hallucination_control, bias_fairness_control,
|
|
212
|
+
)
|
|
107
213
|
else:
|
|
108
|
-
logger.info(
|
|
214
|
+
logger.info("Test case %s auto-approved (score: %.2f)", tc.tc_reference, total)
|
|
109
215
|
|
|
110
216
|
return TestCaseWithConfidence(
|
|
111
217
|
test_case=tc,
|
|
112
218
|
confidence=metrics,
|
|
113
|
-
is_approved=is_approved
|
|
219
|
+
is_approved=is_approved,
|
|
114
220
|
)
|
|
@@ -2,18 +2,98 @@ import ast
|
|
|
2
2
|
import logging
|
|
3
3
|
import subprocess
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Tuple,
|
|
5
|
+
from typing import Tuple, Set
|
|
6
6
|
|
|
7
7
|
from src.db.client import get_conn
|
|
8
|
+
from src.models.test_requirement import ExecutionMetrics
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
12
|
+
|
|
13
|
+
# ── Static Complexity Analyser (lightweight, no external deps) ─────────────────
|
|
14
|
+
|
|
15
|
+
def _compute_cyclomatic_complexity(tree: ast.Module) -> int:
|
|
16
|
+
"""Count decision points to approximate cyclomatic complexity.
|
|
17
|
+
|
|
18
|
+
Counts: if/elif/for/while/try/except/with/assert branches.
|
|
19
|
+
Complexity = 1 + decision_points (baseline 1 per function).
|
|
20
|
+
"""
|
|
21
|
+
decision_nodes = (
|
|
22
|
+
ast.If, ast.For, ast.While, ast.Try,
|
|
23
|
+
ast.ExceptHandler, ast.With, ast.Assert,
|
|
24
|
+
)
|
|
25
|
+
return sum(1 for _ in ast.walk(tree) if isinstance(_, decision_nodes))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compute_duplication_score(code: str) -> float:
|
|
29
|
+
"""Approximate duplication score by detecting repeated non-trivial line blocks.
|
|
30
|
+
|
|
31
|
+
Returns a 0-100 score where 0 = no duplication, 100 = fully duplicated.
|
|
32
|
+
"""
|
|
33
|
+
lines = [l.strip() for l in code.splitlines() if l.strip() and not l.strip().startswith("#")]
|
|
34
|
+
if not lines:
|
|
35
|
+
return 0.0
|
|
36
|
+
seen: dict[str, int] = {}
|
|
37
|
+
for line in lines:
|
|
38
|
+
seen[line] = seen.get(line, 0) + 1
|
|
39
|
+
duplicated = sum(count - 1 for count in seen.values() if count > 1)
|
|
40
|
+
return round(min(100.0, (duplicated / len(lines)) * 100), 2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _count_style_violations(code: str) -> int:
|
|
44
|
+
"""Count basic style violations (magic numbers, overly long lines, missing docstrings).
|
|
45
|
+
|
|
46
|
+
Each violation adds to the Maintainability penalty.
|
|
47
|
+
"""
|
|
48
|
+
violations = 0
|
|
49
|
+
for line in code.splitlines():
|
|
50
|
+
if len(line) > 120:
|
|
51
|
+
violations += 1
|
|
52
|
+
# Detect magic numbers not assigned to a variable
|
|
53
|
+
import re
|
|
54
|
+
if re.search(r"\b(?<![\w=])\d{2,}\b", line) and "=" not in line:
|
|
55
|
+
violations += 1
|
|
56
|
+
return violations
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_maintainability(code: str) -> Tuple[float, float, float, int]:
|
|
60
|
+
"""Maintainability — Ease of modifying and understanding generated scripts.
|
|
61
|
+
|
|
62
|
+
Formula: 100 – ((Complexity Score + Duplication Score + Violations) / Max Score × 100)
|
|
63
|
+
Returns: (maintainability_score 0-1, complexity_raw, duplication_raw, violations_count)
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
tree = ast.parse(code)
|
|
67
|
+
except SyntaxError:
|
|
68
|
+
return 0.0, 0.0, 0.0, 0
|
|
69
|
+
|
|
70
|
+
complexity = _compute_cyclomatic_complexity(tree)
|
|
71
|
+
duplication = _compute_duplication_score(code)
|
|
72
|
+
violations = _count_style_violations(code)
|
|
73
|
+
|
|
74
|
+
# Normalise each sub-score to 0-100 range (caps prevent extreme values)
|
|
75
|
+
MAX_COMPLEXITY = 50.0 # treat 50+ branches as worst case
|
|
76
|
+
MAX_VIOLATIONS = 30.0 # treat 30+ violations as worst case
|
|
77
|
+
|
|
78
|
+
complexity_norm = min(100.0, (complexity / MAX_COMPLEXITY) * 100)
|
|
79
|
+
violation_norm = min(100.0, (violations / MAX_VIOLATIONS) * 100)
|
|
80
|
+
|
|
81
|
+
# Formula (all three sub-scores weighted equally within their sum)
|
|
82
|
+
penalty = (complexity_norm + duplication + violation_norm) / 3.0
|
|
83
|
+
maintainability_pct = max(0.0, 100.0 - penalty)
|
|
84
|
+
maintainability_score = round(maintainability_pct / 100.0, 4)
|
|
85
|
+
|
|
86
|
+
return maintainability_score, round(complexity_norm, 2), round(duplication, 2), violations
|
|
87
|
+
|
|
88
|
+
|
|
11
89
|
class ExecutionValidationAgent:
|
|
12
90
|
"""Multi-stage validation pipeline for generated automation scripts."""
|
|
13
|
-
|
|
91
|
+
|
|
14
92
|
def __init__(self, repo_path: Path):
|
|
15
93
|
self.repo_path = repo_path
|
|
16
|
-
|
|
94
|
+
|
|
95
|
+
# ── Validation Stages ─────────────────────────────────────────────────────
|
|
96
|
+
|
|
17
97
|
def validate_syntax(self, code: str) -> Tuple[bool, str]:
|
|
18
98
|
"""Stage 1: Check Python syntax."""
|
|
19
99
|
try:
|
|
@@ -21,109 +101,147 @@ class ExecutionValidationAgent:
|
|
|
21
101
|
return True, ""
|
|
22
102
|
except SyntaxError as e:
|
|
23
103
|
return False, f"SyntaxError at line {e.lineno}: {e.msg}"
|
|
24
|
-
|
|
104
|
+
|
|
25
105
|
def _get_known_assets(self, asset_type: str) -> Set[str]:
|
|
26
106
|
"""Fetch all known assets of a specific type from the Asset Registry."""
|
|
27
|
-
assets = set()
|
|
107
|
+
assets: Set[str] = set()
|
|
28
108
|
try:
|
|
29
109
|
with get_conn() as conn:
|
|
30
110
|
with conn.cursor() as cur:
|
|
31
|
-
cur.execute(
|
|
111
|
+
cur.execute(
|
|
112
|
+
"SELECT name FROM igel_asset_registry WHERE asset_type = %s",
|
|
113
|
+
(asset_type,),
|
|
114
|
+
)
|
|
32
115
|
for row in cur.fetchall():
|
|
33
116
|
assets.add(row[0])
|
|
34
117
|
except Exception as e:
|
|
35
|
-
logger.error(
|
|
118
|
+
logger.error("Failed to query registry for %s: %s", asset_type, e)
|
|
36
119
|
return assets
|
|
37
120
|
|
|
38
121
|
def validate_imports(self, code: str) -> Tuple[bool, str]:
|
|
39
122
|
"""Stage 2: Check if imported modules exist in the registry."""
|
|
40
123
|
try:
|
|
41
|
-
|
|
124
|
+
ast.parse(code)
|
|
42
125
|
except SyntaxError:
|
|
43
126
|
return False, "Syntax invalid, cannot check imports."
|
|
44
|
-
|
|
45
|
-
# For simplicity, we just extract 'from module import X' and check X
|
|
46
|
-
# A more robust check would analyze the actual repo path.
|
|
47
|
-
missing_imports = []
|
|
48
|
-
known_pos = self._get_known_assets("page_object")
|
|
49
|
-
known_utils = self._get_known_assets("utility")
|
|
50
|
-
known_clients = self._get_known_assets("api_client")
|
|
51
|
-
|
|
52
|
-
all_known = known_pos | known_utils | known_clients
|
|
53
|
-
|
|
54
|
-
for node in ast.walk(tree):
|
|
55
|
-
if isinstance(node, ast.ImportFrom):
|
|
56
|
-
for name in node.names:
|
|
57
|
-
# Ignore standard library and common 3rd party (pytest, allure, typing, etc.)
|
|
58
|
-
if node.module and not any(node.module.startswith(p) for p in ["pytest", "allure", "typing", "selenium", "playwright", "os", "json", "time"]):
|
|
59
|
-
# This is a naive check; in a real enterprise system, you'd match the import path to `file_path`
|
|
60
|
-
pass
|
|
127
|
+
# Registry-aware import validation is advisory; non-blocking.
|
|
61
128
|
return True, ""
|
|
62
|
-
|
|
129
|
+
|
|
63
130
|
def validate_fixtures(self, code: str) -> Tuple[bool, str]:
|
|
64
131
|
"""Stage 3: Check if requested pytest fixtures exist."""
|
|
65
132
|
try:
|
|
66
133
|
tree = ast.parse(code)
|
|
67
134
|
except SyntaxError:
|
|
68
135
|
return False, "Syntax invalid, cannot check fixtures."
|
|
69
|
-
|
|
136
|
+
|
|
70
137
|
known_fixtures = self._get_known_assets("fixture")
|
|
71
|
-
|
|
72
138
|
missing = []
|
|
73
139
|
for node in ast.walk(tree):
|
|
74
140
|
if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"):
|
|
75
|
-
# pytest uses argument names to request fixtures
|
|
76
141
|
for arg in node.args.args:
|
|
77
142
|
arg_name = arg.arg
|
|
78
|
-
|
|
79
|
-
if arg_name not in ["request", "monkeypatch", "capsys", "tmpdir", "self", "cls"]:
|
|
143
|
+
if arg_name not in {"request", "monkeypatch", "capsys", "tmpdir", "self", "cls"}:
|
|
80
144
|
if known_fixtures and arg_name not in known_fixtures:
|
|
81
|
-
missing.append(
|
|
82
|
-
|
|
145
|
+
missing.append(
|
|
146
|
+
f"Fixture '{arg_name}' requested by {node.name} not found in registry."
|
|
147
|
+
)
|
|
83
148
|
if missing:
|
|
84
149
|
return False, "\n".join(missing)
|
|
85
150
|
return True, ""
|
|
86
|
-
|
|
151
|
+
|
|
87
152
|
def validate_pytest_collection(self, file_path: Path) -> Tuple[bool, str]:
|
|
88
153
|
"""Stage 4: Perform a pytest dry-run (collection only)."""
|
|
89
154
|
if not file_path.exists():
|
|
90
155
|
return False, f"File {file_path} does not exist."
|
|
91
|
-
|
|
92
|
-
# Run pytest --collect-only
|
|
93
156
|
try:
|
|
94
|
-
# Note: Requires pytest to be installed in the environment running the server
|
|
95
157
|
result = subprocess.run(
|
|
96
158
|
["pytest", "--collect-only", str(file_path)],
|
|
97
159
|
capture_output=True,
|
|
98
160
|
text=True,
|
|
99
|
-
cwd=str(self.repo_path)
|
|
161
|
+
cwd=str(self.repo_path),
|
|
100
162
|
)
|
|
101
|
-
|
|
163
|
+
if result.returncode == 5:
|
|
164
|
+
return False, "Pytest collected 0 tests."
|
|
102
165
|
if result.returncode != 0:
|
|
103
|
-
# 5 means no tests collected, which is also an error for us
|
|
104
|
-
if result.returncode == 5:
|
|
105
|
-
return False, "Pytest collected 0 tests."
|
|
106
166
|
return False, f"Pytest collection failed:\n{result.stderr or result.stdout}"
|
|
107
|
-
|
|
108
167
|
return True, ""
|
|
109
168
|
except Exception as e:
|
|
110
169
|
return False, f"Error running pytest collection: {e}"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
170
|
+
|
|
171
|
+
# ── Main Pipeline ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def validate_pipeline(self, code: str, file_path: Path) -> Tuple[bool, str, str, ExecutionMetrics]:
|
|
174
|
+
"""Run all execution validation stages and compute ExecutionMetrics.
|
|
175
|
+
|
|
176
|
+
Returns: (passed: bool, status: str, error_msg: str, ExecutionMetrics)
|
|
177
|
+
|
|
178
|
+
ExecutionMetrics computed here:
|
|
179
|
+
- Maintainability – AST-based complexity/duplication formula
|
|
180
|
+
- First-Pass Success – whether collection + syntax pass on first attempt
|
|
181
|
+
- Reusability – populated by automation_design_agent (passed in plan)
|
|
182
|
+
- Flakiness – historical; placeholder (0.0) until HealingAgent populates
|
|
183
|
+
"""
|
|
184
|
+
logger.info("Starting execution validation pipeline for %s", file_path)
|
|
185
|
+
|
|
186
|
+
# ── Maintainability (always computed, even if later stages fail) ──────
|
|
187
|
+
maintainability, complexity_norm, duplication, violations = compute_maintainability(code)
|
|
188
|
+
|
|
116
189
|
ok, err = self.validate_syntax(code)
|
|
117
|
-
if not ok:
|
|
118
|
-
|
|
190
|
+
if not ok:
|
|
191
|
+
exec_metrics = ExecutionMetrics(
|
|
192
|
+
maintainability_score=maintainability,
|
|
193
|
+
complexity_score=complexity_norm,
|
|
194
|
+
duplication_score=duplication,
|
|
195
|
+
violations_count=violations,
|
|
196
|
+
first_pass_success=False,
|
|
197
|
+
)
|
|
198
|
+
return False, "syntax_failed", err, exec_metrics
|
|
199
|
+
|
|
119
200
|
ok, err = self.validate_imports(code)
|
|
120
|
-
if not ok:
|
|
121
|
-
|
|
201
|
+
if not ok:
|
|
202
|
+
exec_metrics = ExecutionMetrics(
|
|
203
|
+
maintainability_score=maintainability,
|
|
204
|
+
complexity_score=complexity_norm,
|
|
205
|
+
duplication_score=duplication,
|
|
206
|
+
violations_count=violations,
|
|
207
|
+
first_pass_success=False,
|
|
208
|
+
)
|
|
209
|
+
return False, "import_failed", err, exec_metrics
|
|
210
|
+
|
|
122
211
|
ok, err = self.validate_fixtures(code)
|
|
123
|
-
if not ok:
|
|
124
|
-
|
|
212
|
+
if not ok:
|
|
213
|
+
exec_metrics = ExecutionMetrics(
|
|
214
|
+
maintainability_score=maintainability,
|
|
215
|
+
complexity_score=complexity_norm,
|
|
216
|
+
duplication_score=duplication,
|
|
217
|
+
violations_count=violations,
|
|
218
|
+
first_pass_success=False,
|
|
219
|
+
)
|
|
220
|
+
return False, "fixture_failed", err, exec_metrics
|
|
221
|
+
|
|
125
222
|
ok, err = self.validate_pytest_collection(file_path)
|
|
126
|
-
if not ok:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
223
|
+
if not ok:
|
|
224
|
+
exec_metrics = ExecutionMetrics(
|
|
225
|
+
maintainability_score=maintainability,
|
|
226
|
+
complexity_score=complexity_norm,
|
|
227
|
+
duplication_score=duplication,
|
|
228
|
+
violations_count=violations,
|
|
229
|
+
first_pass_success=False,
|
|
230
|
+
)
|
|
231
|
+
return False, "collection_failed", err, exec_metrics
|
|
232
|
+
|
|
233
|
+
# All stages passed → First-Pass Success = True
|
|
234
|
+
exec_metrics = ExecutionMetrics(
|
|
235
|
+
maintainability_score=maintainability,
|
|
236
|
+
complexity_score=complexity_norm,
|
|
237
|
+
duplication_score=duplication,
|
|
238
|
+
violations_count=violations,
|
|
239
|
+
first_pass_success=True,
|
|
240
|
+
# reusability_score injected by caller from AutomationPlan.reusability_score
|
|
241
|
+
# flakiness_score injected by HealingAgent over time
|
|
242
|
+
)
|
|
243
|
+
logger.info(
|
|
244
|
+
"Execution validation passed for %s | maintainability=%.2f, first_pass=True",
|
|
245
|
+
file_path, maintainability,
|
|
246
|
+
)
|
|
247
|
+
return True, "passed", "", exec_metrics
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import List, Tuple
|
|
4
4
|
|
|
5
|
-
from src.models.test_requirement import TestRequirement, GeneratedTestCase, TestCaseWithConfidence
|
|
5
|
+
from src.models.test_requirement import TestRequirement, GeneratedTestCase, TestCaseWithConfidence, ExecutionMetrics
|
|
6
6
|
from src.agents.requirement_agent import extract_understanding
|
|
7
7
|
from src.agents.scenario_agent import generate_all_scenarios
|
|
8
8
|
from src.agents.testcase_agent import generate_cases_for_scenario
|
|
@@ -43,7 +43,6 @@ class WorkflowEngine:
|
|
|
43
43
|
resolved_context = EvidenceResolver().resolve(unified_context)
|
|
44
44
|
source_report = unified_context.source_report
|
|
45
45
|
|
|
46
|
-
# In reality, you'd persist `scenarios_with_rules` to DB here
|
|
47
46
|
return req, scenarios_with_rules, resolved_context, source_report
|
|
48
47
|
|
|
49
48
|
def run_testcase_flow(
|
|
@@ -66,11 +65,10 @@ class WorkflowEngine:
|
|
|
66
65
|
# 2. Structural/Traceability Validation
|
|
67
66
|
ok, errs = run_validation_pipeline(tc, sr)
|
|
68
67
|
|
|
69
|
-
# 3. Deterministic Confidence Scoring
|
|
68
|
+
# 3. Deterministic Confidence Scoring (8-parameter framework)
|
|
70
69
|
tc_conf = compute_two_level_confidence(tc, sr, resolved_context, source_report, policy)
|
|
71
70
|
all_cases.append(tc_conf)
|
|
72
71
|
|
|
73
|
-
# Persist to igel_generated_testcases handled by PersistenceEngine now
|
|
74
72
|
return all_cases
|
|
75
73
|
|
|
76
74
|
def run_script_flow(
|
|
@@ -79,14 +77,19 @@ class WorkflowEngine:
|
|
|
79
77
|
test_cases: List[TestCaseWithConfidence],
|
|
80
78
|
output_path: Path,
|
|
81
79
|
plan: AutomationPlan = None,
|
|
82
|
-
) -> Tuple[bool, str, AutomationPlan]:
|
|
83
|
-
"""Approved Test Cases -> (Automation Design) -> Script -> Execution Validation
|
|
80
|
+
) -> Tuple[bool, str, AutomationPlan, ExecutionMetrics]:
|
|
81
|
+
"""Approved Test Cases -> (Automation Design) -> Script -> Execution Validation.
|
|
82
|
+
|
|
83
|
+
Returns: (success, message, plan_used, ExecutionMetrics)
|
|
84
|
+
ExecutionMetrics includes Maintainability, First-Pass Success, and Reusability
|
|
85
|
+
sourced from the AutomationPlan.
|
|
86
|
+
"""
|
|
84
87
|
logger.info(f"Starting script flow for {req.jira_key}")
|
|
85
88
|
|
|
86
89
|
# Filter for approved
|
|
87
90
|
approved = [tc for tc in test_cases if tc.is_approved]
|
|
88
91
|
if not approved:
|
|
89
|
-
return False, "No approved test cases available for automation.", None
|
|
92
|
+
return False, "No approved test cases available for automation.", None, ExecutionMetrics()
|
|
90
93
|
|
|
91
94
|
# 1. Automation Design Agent (reuse a supplied plan if present)
|
|
92
95
|
if plan is None:
|
|
@@ -95,15 +98,17 @@ class WorkflowEngine:
|
|
|
95
98
|
# 2. Script Generator
|
|
96
99
|
ok, code, err = generate_script(test_cases, plan, output_path)
|
|
97
100
|
if not ok:
|
|
98
|
-
return False, err, plan
|
|
101
|
+
return False, err, plan, ExecutionMetrics()
|
|
99
102
|
|
|
100
|
-
# 3. Execution Validation Pipeline
|
|
101
|
-
ok, status, err = self.exec_validator.validate_pipeline(code, output_path)
|
|
103
|
+
# 3. Execution Validation Pipeline — now returns ExecutionMetrics
|
|
104
|
+
ok, status, err, exec_metrics = self.exec_validator.validate_pipeline(code, output_path)
|
|
102
105
|
|
|
103
|
-
#
|
|
106
|
+
# 4. Wire Reusability from AutomationPlan into ExecutionMetrics
|
|
107
|
+
exec_metrics.reusability_score = getattr(plan, "reusability_score", 0.0)
|
|
104
108
|
|
|
105
109
|
if not ok:
|
|
106
110
|
logger.warning(f"Execution validation failed ({status}): {err}")
|
|
107
|
-
return False, err, plan
|
|
111
|
+
return False, err, plan, exec_metrics
|
|
112
|
+
|
|
113
|
+
return True, "Script successfully generated and validated.", plan, exec_metrics
|
|
108
114
|
|
|
109
|
-
return True, "Script successfully generated and validated.", plan
|
|
Binary file
|
package/src/cli/index.ts
CHANGED
|
@@ -21,9 +21,26 @@ program
|
|
|
21
21
|
|
|
22
22
|
// ── helpers ────────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
+
function getSystemPython(): string {
|
|
25
|
+
for (const cmd of ["python3", "python", "py"]) {
|
|
26
|
+
try {
|
|
27
|
+
const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
|
|
28
|
+
if (res.status === 0 || res.status === null) {
|
|
29
|
+
return cmd;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Continue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return "python3";
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
function getPythonBin(): string {
|
|
25
|
-
const
|
|
26
|
-
|
|
39
|
+
const isWin = process.platform === "win32";
|
|
40
|
+
const venv = isWin
|
|
41
|
+
? join(PROJECT_ROOT, ".venv", "Scripts", "python.exe")
|
|
42
|
+
: join(PROJECT_ROOT, ".venv", "bin", "python");
|
|
43
|
+
return existsSync(venv) ? venv : getSystemPython();
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
function resolveRequirementsPath(): string | null {
|
|
@@ -206,7 +223,7 @@ function listFrameworkContextFiles(workspaceRoot: string): string[] {
|
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
function isCopilotCliAvailable(): boolean {
|
|
209
|
-
const check = spawnSync("copilot", ["--version"], { encoding: "utf-8" });
|
|
226
|
+
const check = spawnSync("copilot", ["--version"], { encoding: "utf-8", shell: process.platform === "win32" });
|
|
210
227
|
return check.status === 0;
|
|
211
228
|
}
|
|
212
229
|
|
|
@@ -214,7 +231,7 @@ function isCopilotAuthenticated(workspaceRoot: string): boolean {
|
|
|
214
231
|
const probe = spawnSync(
|
|
215
232
|
"copilot",
|
|
216
233
|
["-C", workspaceRoot, "-p", "Reply with exactly OK.", "--allow-all-tools", "--no-color", "-s"],
|
|
217
|
-
{ encoding: "utf-8" },
|
|
234
|
+
{ encoding: "utf-8", shell: process.platform === "win32" },
|
|
218
235
|
);
|
|
219
236
|
return probe.status === 0;
|
|
220
237
|
}
|
|
@@ -226,7 +243,7 @@ function ensureCopilotLogin(workspaceRoot: string): boolean {
|
|
|
226
243
|
console.log(chalk.yellow(" ⚠ GitHub Copilot CLI is not authenticated."));
|
|
227
244
|
console.log(chalk.yellow(" → Please complete Copilot login to continue context enrichment."));
|
|
228
245
|
|
|
229
|
-
const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8" });
|
|
246
|
+
const login = spawnSync("copilot", ["login"], { stdio: "inherit", encoding: "utf-8", shell: process.platform === "win32" });
|
|
230
247
|
if (login.status !== 0) return false;
|
|
231
248
|
return isCopilotAuthenticated(workspaceRoot);
|
|
232
249
|
}
|
|
@@ -258,7 +275,7 @@ function enrichWithCopilotCli(workspaceRoot: string, contextFiles: string[]): {
|
|
|
258
275
|
"--no-color",
|
|
259
276
|
"-s",
|
|
260
277
|
],
|
|
261
|
-
{ encoding: "utf-8" },
|
|
278
|
+
{ encoding: "utf-8", shell: process.platform === "win32" },
|
|
262
279
|
);
|
|
263
280
|
|
|
264
281
|
if (result.status === 0) {
|
|
@@ -376,7 +393,7 @@ program
|
|
|
376
393
|
if (!existsSync(venvPath)) {
|
|
377
394
|
console.log(chalk.yellow("1. Creating virtual environment…"));
|
|
378
395
|
try {
|
|
379
|
-
execSync(
|
|
396
|
+
execSync(`${getSystemPython()} -m venv .venv`, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
380
397
|
console.log(chalk.green(" ✓ venv created"));
|
|
381
398
|
} catch (e: unknown) {
|
|
382
399
|
console.log(chalk.yellow(` ⚠ venv setup skipped: ${e instanceof Error ? e.message : String(e)}`));
|
package/src/cli/workflow_cli.py
CHANGED
|
@@ -496,7 +496,7 @@ def main():
|
|
|
496
496
|
logging.warning("Failed to read automation plan artifact; will design fresh: %s", e)
|
|
497
497
|
|
|
498
498
|
output_script = output_dir / f"test_{jira_key.replace('-', '_').lower()}.py"
|
|
499
|
-
ok, msg, used_plan = engine.run_script_flow(req, approved, output_script, plan=automation_plan)
|
|
499
|
+
ok, msg, used_plan, exec_metrics = engine.run_script_flow(req, approved, output_script, plan=automation_plan)
|
|
500
500
|
|
|
501
501
|
if ok and not automation_plan_used and used_plan is not None:
|
|
502
502
|
try:
|
|
@@ -521,6 +521,16 @@ def main():
|
|
|
521
521
|
"stage": "automation_plan",
|
|
522
522
|
"approval_checked": bool(cfg.V2_ORCHESTRATOR_ENABLED or cfg.V2_GOVERNANCE_ENFORCED or cfg.V2_APPROVAL_MANAGER_ENABLED),
|
|
523
523
|
},
|
|
524
|
+
# ── Execution & Quality Metrics ───────────────────────────
|
|
525
|
+
"execution_metrics": {
|
|
526
|
+
"maintainability_score": round(exec_metrics.maintainability_score * 100, 2),
|
|
527
|
+
"complexity_score": exec_metrics.complexity_score,
|
|
528
|
+
"duplication_score": exec_metrics.duplication_score,
|
|
529
|
+
"violations_count": exec_metrics.violations_count,
|
|
530
|
+
"first_pass_success": exec_metrics.first_pass_success,
|
|
531
|
+
"reusability_score": round(exec_metrics.reusability_score * 100, 2),
|
|
532
|
+
"flakiness_score": round(exec_metrics.flakiness_score * 100, 2),
|
|
533
|
+
},
|
|
524
534
|
"context_projection": _best_effort_project_context(repo_path),
|
|
525
535
|
})
|
|
526
536
|
else:
|
|
@@ -28,6 +28,7 @@ pydantic>=2.7.0
|
|
|
28
28
|
eval_type_backport>=0.2.0; python_version < "3.10"
|
|
29
29
|
tenacity==9.0.0
|
|
30
30
|
rich==13.9.4
|
|
31
|
+
PyYAML>=6.0.1
|
|
31
32
|
|
|
32
33
|
# OSS local reranker (CrossEncoder — free, runs on CPU/GPU)
|
|
33
34
|
# Default model: BAAI/bge-reranker-v2-m3 (multilingual, best open-source for IGEL DE/EN)
|
package/src/mcp/server.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import { spawn } from "child_process";
|
|
6
|
+
import { spawn, spawnSync } from "child_process";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import fs from "fs";
|
|
@@ -14,17 +14,31 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
14
14
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
15
15
|
|
|
16
16
|
// Resolve the correct Python executable:
|
|
17
|
-
// 1. Check for a local .venv
|
|
18
|
-
// 2. Fall back to system python3 / python
|
|
17
|
+
// 1. Check for a local .venv (Linux or Windows path layouts)
|
|
18
|
+
// 2. Fall back to system python3 / python / py
|
|
19
19
|
function resolvePythonExecutable(): string {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const isWin = process.platform === 'win32';
|
|
21
|
+
const candidates = isWin
|
|
22
|
+
? [
|
|
23
|
+
path.join(PROJECT_ROOT, '.venv', 'Scripts', 'python.exe'),
|
|
24
|
+
path.join(PROJECT_ROOT, '.venv', 'Scripts', 'python'),
|
|
25
|
+
]
|
|
26
|
+
: [
|
|
27
|
+
path.join(PROJECT_ROOT, '.venv', 'bin', 'python'),
|
|
28
|
+
path.join(PROJECT_ROOT, '.venv', 'bin', 'python3'),
|
|
29
|
+
];
|
|
24
30
|
for (const c of candidates) {
|
|
25
31
|
if (fs.existsSync(c)) return c;
|
|
26
32
|
}
|
|
27
|
-
// Check system python3 / python
|
|
33
|
+
// Check system python3 / python / py
|
|
34
|
+
for (const cmd of ['python3', 'python', 'py']) {
|
|
35
|
+
try {
|
|
36
|
+
const res = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
|
|
37
|
+
if (res.status === 0 || res.status === null) {
|
|
38
|
+
return cmd;
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
28
42
|
return 'python3';
|
|
29
43
|
}
|
|
30
44
|
|
|
Binary file
|
|
@@ -75,13 +75,23 @@ class ConfidenceMetrics(BaseModel):
|
|
|
75
75
|
"""Deterministic two-level confidence score breakdown."""
|
|
76
76
|
context_strength: float = 0.0
|
|
77
77
|
requirement_clarity: float = 0.0
|
|
78
|
-
|
|
78
|
+
hallucination_control: float = 0.0
|
|
79
79
|
business_rule_coverage: float = 0.0
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
contextual_relevance: float = 0.0
|
|
81
|
+
bias_fairness_control: float = 0.0
|
|
82
82
|
structural_quality: float = 0.0
|
|
83
83
|
final_confidence: float = 0.0
|
|
84
84
|
|
|
85
|
+
class ExecutionMetrics(BaseModel):
|
|
86
|
+
"""Post-execution metrics for automation scripts."""
|
|
87
|
+
maintainability_score: float = 0.0
|
|
88
|
+
complexity_score: float = 0.0
|
|
89
|
+
duplication_score: float = 0.0
|
|
90
|
+
violations_count: int = 0
|
|
91
|
+
first_pass_success: bool = False
|
|
92
|
+
reusability_score: float = 0.0
|
|
93
|
+
flakiness_score: float = 0.0
|
|
94
|
+
|
|
85
95
|
class TestCaseWithConfidence(BaseModel):
|
|
86
96
|
"""Test case bundled with its validation and confidence metrics."""
|
|
87
97
|
test_case: GeneratedTestCase
|