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 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 venv = join(PROJECT_ROOT, ".venv", "bin", "python");
20
- return existsSync(venv) ? venv : "python3";
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(`python3 -m venv .venv`, { cwd: PROJECT_ROOT, stdio: "inherit" });
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) {
@@ -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 with a Linux bin/ (created by python3 -m 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 candidates = [
18
- path.join(PROJECT_ROOT, '.venv', 'bin', 'python'),
19
- path.join(PROJECT_ROOT, '.venv', 'bin', 'python3'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "igel-qe-core",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "IGEL QE Developer Experience Layer (CLI & MCP)",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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=data.get("page_objects", []),
112
- fixtures=data.get("fixtures", []),
113
- utilities=data.get("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, data, and expected results."""
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: step_score += 0.4
24
- if step.data: step_score += 0.2
25
- if step.expected_result: step_score += 0.4
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
- Computes two levels of confidence using purely deterministic structural checks.
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
- # 1. Context Strength (Informational)
150
+ # ── Pre-Execution Dimensions ──────────────────────────────────────────────
151
+
152
+ # 1. Context Strength (informational, not weighted)
43
153
  sources_found = sum([
44
- source_report.jira_found, source_report.confluence_found,
45
- source_report.postgres_kb_found, source_report.igel_os12_docs_found,
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. Generation Confidence: Requirement Clarity (25%)
50
- # Deterministic check: Are ACs, Business Rules, Dependencies, Environment present in the structured scenario?
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. Generation Confidence: Evidence Quality (20%)
56
- # Deterministic: Based on resolved conflicts
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. Generation Confidence: Business Rule Coverage (15%)
61
- # Deterministic: Number of extracted rule objects vs mapped steps
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. Generation Confidence: Traceability Coverage (15%)
65
- # Deterministic: Do steps have mapped requirement IDs? (Proxy using tc_reference mapped)
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. Generation Confidence: Scenario Coverage (15%)
69
- # Deterministic: Does the generated suite contain Positive + Negative + Edge tags?
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. Generation Confidence: Structural Quality (10%)
74
- # Deterministic: Preconditions, steps, expected results (existing validation_agent.py)
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 total (Final Confidence)
180
+ # ── Weighted Final Score ──────────────────────────────────────────────────
181
+ # Weights pulled from policy dimensions; field names updated to match new params
79
182
  total = (
80
- requirement_clarity * dims.requirement_clarity +
81
- evidence_quality * dims.evidence_quality +
82
- rule_coverage * dims.business_rule_coverage +
83
- traceability * dims.traceability_coverage +
84
- scenario_score * dims.scenario_coverage +
85
- structural_quality * dims.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
- evidence_quality=evidence_quality,
92
- business_rule_coverage=rule_coverage,
93
- traceability_coverage=traceability,
94
- scenario_coverage=scenario_score,
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, 'model_dump') else metrics.dict()
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(f"Test case {tc.tc_reference} needs review (score: {total:.2f})")
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(f"Test case {tc.tc_reference} auto-approved (score: {total:.2f})")
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, List, Set
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("SELECT name FROM igel_asset_registry WHERE asset_type = %s", (asset_type,))
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(f"Failed to query registry for {asset_type}: {e}")
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
- tree = ast.parse(code)
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
- # Ignore common built-in fixtures
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(f"Fixture '{arg_name}' requested by {node.name} not found in registry.")
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
- def validate_pipeline(self, code: str, file_path: Path) -> Tuple[bool, str, str]:
113
- """Run all execution validation stages."""
114
- logger.info(f"Starting execution validation pipeline for {file_path}")
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: return False, "syntax_failed", err
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: return False, "import_failed", err
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: return False, "fixture_failed", err
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: return False, "collection_failed", err
127
-
128
- logger.info(f"Execution validation passed for {file_path}")
129
- return True, "passed", ""
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
- # Persist to igel_automation_scripts here
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
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 venv = join(PROJECT_ROOT, ".venv", "bin", "python");
26
- return existsSync(venv) ? venv : "python3";
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(`python3 -m venv .venv`, { cwd: PROJECT_ROOT, stdio: "inherit" });
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)}`));
@@ -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 with a Linux bin/ (created by python3 -m 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 candidates = [
21
- path.join(PROJECT_ROOT, '.venv', 'bin', 'python'),
22
- path.join(PROJECT_ROOT, '.venv', 'bin', 'python3'),
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
 
@@ -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
- evidence_quality: float = 0.0
78
+ hallucination_control: float = 0.0
79
79
  business_rule_coverage: float = 0.0
80
- traceability_coverage: float = 0.0
81
- scenario_coverage: float = 0.0
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