synapse-hub 0.0.0-stage → 2.0.0
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/.agents/agents/failure-analyst.md +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +394 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +418 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise QA Project Analyzer & Intelligence Aggregator
|
|
4
|
+
---------------------------------------------------------
|
|
5
|
+
Consolidates all project diagnostic and AST analysis tools into a single workflow.
|
|
6
|
+
Dispatched when user requests: 'analyze project', 're-analyze', 'audit project'.
|
|
7
|
+
|
|
8
|
+
Executes in sequence:
|
|
9
|
+
1. Environment & Structure Diagnostic (tools/check_env.py)
|
|
10
|
+
2. Page Class & AST Repository Mapping (tools/repo_map.py)
|
|
11
|
+
3. Static Defect & Fake-Pass Sentinel (tools/infer_linter.py)
|
|
12
|
+
4. Data Flow Lifecycle & Teardown Audit (tools/trace_data_flow.py)
|
|
13
|
+
5. Definition of Done 7 Quality Gates (tools/verify_all.py)
|
|
14
|
+
|
|
15
|
+
Generates & Updates In-Place inside the Target Project:
|
|
16
|
+
- PROJECT_HEALTH_REPORT.md (Root & reports/ of target project)
|
|
17
|
+
- repo_map.txt (AST method map inside target project root)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import json
|
|
23
|
+
import argparse
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
28
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
31
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
37
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
38
|
+
|
|
39
|
+
import check_env
|
|
40
|
+
import repo_map
|
|
41
|
+
import infer_linter
|
|
42
|
+
import trace_data_flow
|
|
43
|
+
import verify_all
|
|
44
|
+
|
|
45
|
+
# ANSI formatting
|
|
46
|
+
GREEN = "\033[92m"
|
|
47
|
+
RED = "\033[91m"
|
|
48
|
+
YELLOW = "\033[93m"
|
|
49
|
+
CYAN = "\033[96m"
|
|
50
|
+
BOLD = "\033[1m"
|
|
51
|
+
RESET = "\033[0m"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
55
|
+
chosen = cli_repo or cli_target
|
|
56
|
+
if chosen:
|
|
57
|
+
resolved = os.path.abspath(chosen)
|
|
58
|
+
if not os.path.exists(resolved):
|
|
59
|
+
raise FileNotFoundError(f"Target repository path not found: {resolved}")
|
|
60
|
+
return resolved
|
|
61
|
+
|
|
62
|
+
env_dir = os.environ.get("TARGET_PROJECT_DIR")
|
|
63
|
+
if env_dir and os.path.exists(env_dir):
|
|
64
|
+
return os.path.abspath(env_dir)
|
|
65
|
+
|
|
66
|
+
cwd = os.path.abspath(os.getcwd())
|
|
67
|
+
if cwd.lower() != HUB_ROOT.lower():
|
|
68
|
+
return cwd
|
|
69
|
+
|
|
70
|
+
return HUB_ROOT
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_full_analysis(target_repo):
|
|
74
|
+
"""Executes the complete diagnostic and AST intelligence suite."""
|
|
75
|
+
start_time = datetime.now()
|
|
76
|
+
mode = verify_all.detect_repo_mode(target_repo)
|
|
77
|
+
|
|
78
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
79
|
+
print(f"{BOLD}{CYAN} ENTERPRISE QA - COMPREHENSIVE PROJECT ANALYSIS SUITE {RESET}")
|
|
80
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
81
|
+
print(f"📁 Target Repository : {target_repo}")
|
|
82
|
+
print(f"🏷️ Operational Mode : {mode}")
|
|
83
|
+
print(f"⏱️ Timestamp : {start_time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
84
|
+
|
|
85
|
+
summary = {
|
|
86
|
+
"timestamp": start_time.isoformat(),
|
|
87
|
+
"target_repo": target_repo,
|
|
88
|
+
"mode": mode,
|
|
89
|
+
"env_status": "UNKNOWN",
|
|
90
|
+
"repo_map": {},
|
|
91
|
+
"infer_linter": {},
|
|
92
|
+
"data_flow": {},
|
|
93
|
+
"gates_status": "UNKNOWN"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Step 1: Environment & Project Structure
|
|
97
|
+
print(f"{BOLD}[1/5] Checking Environment & Project Structure (check_env)...{RESET}")
|
|
98
|
+
env_runner = check_env.DiagnosticRunner(target_dir=target_repo)
|
|
99
|
+
env_code = env_runner.run_all()
|
|
100
|
+
summary["env_status"] = "READY" if env_code == 0 else "WARNINGS_OR_MISSING"
|
|
101
|
+
|
|
102
|
+
# Step 2: Page Classes & Method AST Mapping
|
|
103
|
+
print(f"\n{BOLD}[2/5] Mapping Page Classes & API Signatures (repo_map)...{RESET}")
|
|
104
|
+
try:
|
|
105
|
+
map_stats = repo_map.build_repo_map(target_repo=target_repo)
|
|
106
|
+
summary["repo_map"] = {
|
|
107
|
+
"classes_count": map_stats["classes_count"],
|
|
108
|
+
"methods_count": map_stats["methods_count"],
|
|
109
|
+
"source_lines": map_stats["source_lines"],
|
|
110
|
+
"output_file": map_stats["output_file"]
|
|
111
|
+
}
|
|
112
|
+
print(f" -> Indexed {map_stats['classes_count']} Page Classes ({map_stats['methods_count']} API signatures).")
|
|
113
|
+
print(f" -> Condensed context map: {map_stats['output_file']}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f" ⚠️ repo_map error: {e}")
|
|
116
|
+
|
|
117
|
+
# Step 3: Static Defect & Fake-Pass Sentinel
|
|
118
|
+
print(f"\n{BOLD}[3/5] Scanning Static Defects & Fake-Passes (infer_linter)...{RESET}")
|
|
119
|
+
scripts_dir = os.path.join(target_repo, "Scripts")
|
|
120
|
+
try:
|
|
121
|
+
linter_code = infer_linter.run_infer_linter(target_dir=scripts_dir)
|
|
122
|
+
summary["infer_linter"]["exit_code"] = linter_code
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f" ⚠️ infer_linter error: {e}")
|
|
125
|
+
|
|
126
|
+
# Step 4: Data Flow Lifecycle & Teardown Hygiene
|
|
127
|
+
print(f"\n{BOLD}[4/5] Auditing Synthetic Data Lifecycle & Teardown (trace_data_flow)...{RESET}")
|
|
128
|
+
try:
|
|
129
|
+
data_flow_code = trace_data_flow.run_data_flow_tracer(target_dir=scripts_dir, repo_root=target_repo)
|
|
130
|
+
summary["data_flow"]["exit_code"] = data_flow_code
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f" ⚠️ trace_data_flow error: {e}")
|
|
133
|
+
|
|
134
|
+
# Step 5: 7 Quality Gates DoD Verification
|
|
135
|
+
print(f"\n{BOLD}[5/5] Verifying 7 Quality Gates DoD (verify_all)...{RESET}")
|
|
136
|
+
gates_passed = True
|
|
137
|
+
gates = [
|
|
138
|
+
("Gate 1: UTF-8 No BOM", verify_all.gate_1_bom_check),
|
|
139
|
+
("Gate 2: Path Portability", verify_all.gate_2_path_portability),
|
|
140
|
+
("Gate 3: Clean POM & Flat Architecture", verify_all.gate_3_clean_pom),
|
|
141
|
+
("Gate 4: AI Documentation Linter", verify_all.gate_4_ai_docs_lint),
|
|
142
|
+
("Gate 5: Temporary Files Blacklist", verify_all.gate_5_cleanup_blacklist),
|
|
143
|
+
("Gate 6: Credential Security", verify_all.gate_6_credential_security),
|
|
144
|
+
("Gate 7: Static Defect Sentinel", verify_all.gate_7_infer_linter),
|
|
145
|
+
]
|
|
146
|
+
for name, gate_fn in gates:
|
|
147
|
+
if not gate_fn(target_repo, mode=mode):
|
|
148
|
+
gates_passed = False
|
|
149
|
+
|
|
150
|
+
summary["gates_status"] = "100%_PASS" if gates_passed else "FAILURES_DETECTED"
|
|
151
|
+
|
|
152
|
+
# Generate Markdown Executive Report
|
|
153
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
154
|
+
reports_dir = os.path.join(target_repo, "reports") if mode == "TARGET" else os.path.join(HUB_ROOT, "reports")
|
|
155
|
+
os.makedirs(reports_dir, exist_ok=True)
|
|
156
|
+
report_in_reports = os.path.join(reports_dir, "PROJECT_HEALTH_REPORT.md")
|
|
157
|
+
report_in_root = os.path.join(target_repo, "PROJECT_HEALTH_REPORT.md")
|
|
158
|
+
|
|
159
|
+
md_content = f"""# Project Health & AST Analysis Report
|
|
160
|
+
|
|
161
|
+
- **Target Repository:** `{target_repo}`
|
|
162
|
+
- **Operational Mode:** `{mode}`
|
|
163
|
+
- **Execution Timestamp:** `{start_time.strftime('%Y-%m-%d %H:%M:%S')}` (Completed in `{elapsed:.2f}s`)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 1. Executive Health Metrics
|
|
168
|
+
|
|
169
|
+
| Metric / Standard | Result | Status |
|
|
170
|
+
| :--- | :---: | :---: |
|
|
171
|
+
| **Environment & Structure** | `{summary['env_status']}` | {'✅ PASS' if summary['env_status'] == 'READY' else '⚠️ WARN'} |
|
|
172
|
+
| **Page Classes & Helpers** | `{summary['repo_map'].get('classes_count', 0)}` classes (`{summary['repo_map'].get('methods_count', 0)}` methods) | ✅ INDEXED |
|
|
173
|
+
| **Static Defect Sentinel** | `infer_linter` exit `{summary['infer_linter'].get('exit_code', 0)}` | {'✅ CLEAN' if summary['infer_linter'].get('exit_code', 0) == 0 else '❌ CRITICAL'} |
|
|
174
|
+
| **Lifecycle & Teardown** | `trace_data_flow` exit `{summary['data_flow'].get('exit_code', 0)}` | ✅ AUDITED |
|
|
175
|
+
| **7 Quality Gates DoD** | `{summary['gates_status']}` | {'✅ 100% PASS' if gates_passed else '❌ BLOCKED'} |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 2. In-Project Synchronized Artifacts
|
|
180
|
+
|
|
181
|
+
1. **Executive Health Summary (This file):** `PROJECT_HEALTH_REPORT.md`
|
|
182
|
+
2. **Condensed API Signatures Map (< 600 tokens):** `repo_map.txt`
|
|
183
|
+
3. **AST Static Defect Log:** `reports/infer_linter_report.json`
|
|
184
|
+
4. **Data Lifecycle Taint Audit:** `reports/data_flow_trace_report.json`
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 3. AI Context Anchoring & Post-Audit Protocol (0-Token Overhead)
|
|
189
|
+
|
|
190
|
+
> 📌 **AI Agent Guidelines:**
|
|
191
|
+
> - In subsequent sessions on this project, **DO NOT rescan** the entire codebase from scratch.
|
|
192
|
+
> - Anchor context directly by reading only two local in-project artifacts:
|
|
193
|
+
> 1. `PROJECT_HEALTH_REPORT.md` (immediate architecture status, warnings, and DoD readiness).
|
|
194
|
+
> 2. `repo_map.txt` (instant lookup for Page Classes, methods, and parameters in < 600 tokens).
|
|
195
|
+
> - **Post-Audit Git Push (Mandatory):** Once analysis completes, stage, commit, and push updated in-project artifacts to git (`git add PROJECT_HEALTH_REPORT.md repo_map.txt && git commit -m "docs(audit): update project health report and AST repo map" && git push origin <branch>`).
|
|
196
|
+
> - Whenever the user requests **"analyze project"** or **"project audit"**, re-run `agent-qa analyze` to update this file in-place.
|
|
197
|
+
"""
|
|
198
|
+
with open(report_in_reports, "w", encoding="utf-8") as f:
|
|
199
|
+
f.write(md_content)
|
|
200
|
+
|
|
201
|
+
# Save directly at the root of target_repo as well so it's instantly discoverable
|
|
202
|
+
if os.path.abspath(target_repo).lower() != os.path.abspath(HUB_ROOT).lower():
|
|
203
|
+
try:
|
|
204
|
+
with open(report_in_root, "w", encoding="utf-8") as f:
|
|
205
|
+
f.write(md_content)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
210
|
+
print(f"{BOLD}{CYAN} EXECUTIVE ANALYSIS COMPLETE {RESET}")
|
|
211
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
212
|
+
print(f" Elapsed Time : {elapsed:.2f}s")
|
|
213
|
+
print(f" Page Classes Indexed : {summary['repo_map'].get('classes_count', 0)} classes")
|
|
214
|
+
print(f" 7 Quality Gates DoD : {GREEN + '100% PASS' if gates_passed else RED + 'FAILED'}{RESET}")
|
|
215
|
+
print(f" In-Project Report Path : {report_in_root if os.path.abspath(target_repo).lower() != os.path.abspath(HUB_ROOT).lower() else report_in_reports}")
|
|
216
|
+
if os.path.abspath(target_repo).lower() != os.path.abspath(HUB_ROOT).lower():
|
|
217
|
+
print(f" {YELLOW}💡 Post-Analysis Action: Push updated files to Git:{RESET}")
|
|
218
|
+
print(f" git add PROJECT_HEALTH_REPORT.md repo_map.txt")
|
|
219
|
+
print(f" git commit -m \"docs(audit): update project health report and AST repo map\"")
|
|
220
|
+
print(f" git push origin <branch>")
|
|
221
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
222
|
+
|
|
223
|
+
return 0 if gates_passed else 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
parser = argparse.ArgumentParser(description="Comprehensive Enterprise QA Project Analyzer")
|
|
228
|
+
parser.add_argument("target", nargs="?", default=None, help="Target repository path to analyze")
|
|
229
|
+
parser.add_argument("--repo", "--path", dest="repo_path", default=None, help="Target repository path")
|
|
230
|
+
args = parser.parse_args()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
target_repo = resolve_target_repo(cli_target=args.target, cli_repo=args.repo_path)
|
|
234
|
+
except FileNotFoundError as e:
|
|
235
|
+
print(f"❌ Error: {e}")
|
|
236
|
+
return 1
|
|
237
|
+
|
|
238
|
+
return run_full_analysis(target_repo)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
sys.exit(main())
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise API Contract Drift & Breaking Change Sentinel
|
|
4
|
+
---------------------------------------------------------
|
|
5
|
+
Compares OpenAPI / Swagger specifications (v2 / v3) across versions to detect
|
|
6
|
+
breaking changes (deleted endpoints, removed fields, newly required parameters, type shifts)
|
|
7
|
+
before UI test suites run and fail due to backend contract drift.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import argparse
|
|
14
|
+
from typing import Dict, List, Any, Tuple
|
|
15
|
+
|
|
16
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
17
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
18
|
+
try:
|
|
19
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
20
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_spec(file_path: str) -> Dict[str, Any]:
|
|
26
|
+
"""Loads an OpenAPI/Swagger spec from a JSON file."""
|
|
27
|
+
if not os.path.exists(file_path):
|
|
28
|
+
raise FileNotFoundError(f"Spec file not found: {file_path}")
|
|
29
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compare_specs(old_spec: Dict[str, Any], new_spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Computes breaking changes and non-breaking contract modifications between two specs.
|
|
36
|
+
"""
|
|
37
|
+
breaking: List[Dict[str, str]] = []
|
|
38
|
+
warnings: List[Dict[str, str]] = []
|
|
39
|
+
info: List[Dict[str, str]] = []
|
|
40
|
+
|
|
41
|
+
old_paths = old_spec.get("paths", {})
|
|
42
|
+
new_paths = new_spec.get("paths", {})
|
|
43
|
+
|
|
44
|
+
# 1. Check for removed endpoints (Paths)
|
|
45
|
+
for path, old_methods in old_paths.items():
|
|
46
|
+
if path not in new_paths:
|
|
47
|
+
breaking.append({
|
|
48
|
+
"type": "REMOVED_ENDPOINT",
|
|
49
|
+
"path": path,
|
|
50
|
+
"detail": f"Entire endpoint path was deleted: '{path}'"
|
|
51
|
+
})
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
new_methods = new_paths[path]
|
|
55
|
+
|
|
56
|
+
# 2. Check for removed HTTP methods on existing path
|
|
57
|
+
for method, old_op in old_methods.items():
|
|
58
|
+
if method.lower() in ("parameters", "$ref", "summary", "description"):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if method not in new_methods:
|
|
62
|
+
breaking.append({
|
|
63
|
+
"type": "REMOVED_HTTP_METHOD",
|
|
64
|
+
"path": f"{method.upper()} {path}",
|
|
65
|
+
"detail": f"HTTP method '{method.upper()}' was deleted from path '{path}'"
|
|
66
|
+
})
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
new_op = new_methods[method]
|
|
70
|
+
|
|
71
|
+
# 3. Check Parameters
|
|
72
|
+
old_params = {p.get("name"): p for p in old_op.get("parameters", []) if isinstance(p, dict)}
|
|
73
|
+
new_params = {p.get("name"): p for p in new_op.get("parameters", []) if isinstance(p, dict)}
|
|
74
|
+
|
|
75
|
+
# Check for newly added REQUIRED parameters (Breaks existing clients)
|
|
76
|
+
for p_name, p_info in new_params.items():
|
|
77
|
+
if p_name not in old_params and p_info.get("required") is True:
|
|
78
|
+
breaking.append({
|
|
79
|
+
"type": "NEW_REQUIRED_PARAM",
|
|
80
|
+
"path": f"{method.upper()} {path}",
|
|
81
|
+
"detail": f"Added newly required parameter '{p_name}' in '{p_info.get('in', 'query')}'"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# Check for changed parameter types or newly required existing parameters
|
|
85
|
+
for p_name, old_p in old_params.items():
|
|
86
|
+
if p_name in new_params:
|
|
87
|
+
new_p = new_params[p_name]
|
|
88
|
+
if not old_p.get("required") and new_p.get("required") is True:
|
|
89
|
+
breaking.append({
|
|
90
|
+
"type": "PARAM_NOW_REQUIRED",
|
|
91
|
+
"path": f"{method.upper()} {path}",
|
|
92
|
+
"detail": f"Parameter '{p_name}' was changed from optional to required"
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
old_type = old_p.get("type") or old_p.get("schema", {}).get("type")
|
|
96
|
+
new_type = new_p.get("type") or new_p.get("schema", {}).get("type")
|
|
97
|
+
if old_type and new_type and old_type != new_type:
|
|
98
|
+
breaking.append({
|
|
99
|
+
"type": "PARAM_TYPE_CHANGED",
|
|
100
|
+
"path": f"{method.upper()} {path}",
|
|
101
|
+
"detail": f"Parameter '{p_name}' type shifted from '{old_type}' to '{new_type}'"
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# 4. Check Responses (Removed expected status codes)
|
|
105
|
+
old_resps = old_op.get("responses", {})
|
|
106
|
+
new_resps = new_op.get("responses", {})
|
|
107
|
+
for status_code, old_resp in old_resps.items():
|
|
108
|
+
if status_code in ("200", "201", "204") and status_code not in new_resps:
|
|
109
|
+
warnings.append({
|
|
110
|
+
"type": "REMOVED_SUCCESS_RESPONSE",
|
|
111
|
+
"path": f"{method.upper()} {path}",
|
|
112
|
+
"detail": f"Success response code '{status_code}' was removed or modified"
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# Non-breaking additions
|
|
116
|
+
for path, new_methods in new_paths.items():
|
|
117
|
+
if path not in old_paths:
|
|
118
|
+
info.append({
|
|
119
|
+
"type": "NEW_ENDPOINT_ADDED",
|
|
120
|
+
"path": path,
|
|
121
|
+
"detail": f"New endpoint added: '{path}' (Backward compatible)"
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
health_score = 100.0 if not breaking else max(0.0, 100.0 - (len(breaking) * 20.0))
|
|
125
|
+
is_compatible = len(breaking) == 0
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"is_backward_compatible": is_compatible,
|
|
129
|
+
"contract_health_score": health_score,
|
|
130
|
+
"breaking_changes_count": len(breaking),
|
|
131
|
+
"warnings_count": len(warnings),
|
|
132
|
+
"additions_count": len(info),
|
|
133
|
+
"breaking_changes": breaking,
|
|
134
|
+
"warnings": warnings,
|
|
135
|
+
"additions": info
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def generate_markdown_report(analysis: Dict[str, Any], old_path: str, new_path: str) -> str:
|
|
140
|
+
status_icon = "✅ COMPATIBLE" if analysis["is_backward_compatible"] else "❌ BREAKING DRIFT DETECTED"
|
|
141
|
+
lines = [
|
|
142
|
+
"# API Contract Drift & Breaking Change Report",
|
|
143
|
+
f"- **Baseline Spec:** `{old_path}`",
|
|
144
|
+
f"- **Current Spec:** `{new_path}`",
|
|
145
|
+
f"- **Verdict:** {status_icon} (Health Score: `{analysis['contract_health_score']:.1f}%`)",
|
|
146
|
+
"",
|
|
147
|
+
"## Summary Metrics",
|
|
148
|
+
f"| Breaking Changes | Warnings | Compatible Additions |",
|
|
149
|
+
f"| :---: | :---: | :---: |",
|
|
150
|
+
f"| **{analysis['breaking_changes_count']}** | **{analysis['warnings_count']}** | **{analysis['additions_count']}** |",
|
|
151
|
+
""
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
if analysis["breaking_changes"]:
|
|
155
|
+
lines.append("## ❌ Breaking Changes (Action Required)")
|
|
156
|
+
lines.append("| Type | Endpoint / Method | Root Cause Detail |")
|
|
157
|
+
lines.append("| :--- | :--- | :--- |")
|
|
158
|
+
for b in analysis["breaking_changes"]:
|
|
159
|
+
lines.append(f"| `{b['type']}` | `{b['path']}` | {b['detail']} |")
|
|
160
|
+
lines.append("")
|
|
161
|
+
|
|
162
|
+
if analysis["warnings"]:
|
|
163
|
+
lines.append("## ⚠️ Warnings & Potential Regressions")
|
|
164
|
+
for w in analysis["warnings"]:
|
|
165
|
+
lines.append(f"- **{w['path']}**: {w['detail']}")
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
if analysis["additions"]:
|
|
169
|
+
lines.append("## ℹ️ Compatible New Additions")
|
|
170
|
+
for a in analysis["additions"]:
|
|
171
|
+
lines.append(f"- **{a['path']}**: {a['detail']}")
|
|
172
|
+
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main():
|
|
177
|
+
parser = argparse.ArgumentParser(description="API Contract Drift & Schema Diff Sentinel")
|
|
178
|
+
parser.add_argument("--old", "--baseline", dest="old_spec", required=True, help="Path to baseline OpenAPI/Swagger JSON")
|
|
179
|
+
parser.add_argument("--new", "--current", dest="new_spec", required=True, help="Path to current OpenAPI/Swagger JSON")
|
|
180
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON results")
|
|
181
|
+
parser.add_argument("-o", "--output", dest="output_file", default=None, help="Save report to file")
|
|
182
|
+
args = parser.parse_args()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
old_data = load_spec(args.old_spec)
|
|
186
|
+
new_data = load_spec(args.new_spec)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"❌ Error loading specs: {e}")
|
|
189
|
+
return 1
|
|
190
|
+
|
|
191
|
+
analysis = compare_specs(old_data, new_data)
|
|
192
|
+
md_report = generate_markdown_report(analysis, args.old_spec, args.new_spec)
|
|
193
|
+
|
|
194
|
+
if args.output_file:
|
|
195
|
+
with open(args.output_file, "w", encoding="utf-8") as f:
|
|
196
|
+
f.write(md_report)
|
|
197
|
+
print(f"✅ Drift report saved to: {args.output_file}")
|
|
198
|
+
|
|
199
|
+
if args.json:
|
|
200
|
+
print(json.dumps(analysis, indent=2))
|
|
201
|
+
else:
|
|
202
|
+
print(md_report)
|
|
203
|
+
|
|
204
|
+
return 0 if analysis["is_backward_compatible"] else 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main())
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Closed-Loop Autonomous Test Healer Engine
|
|
4
|
+
-----------------------------------------------------
|
|
5
|
+
Diagnoses test failures in 0.1s from JUnit XML and terminal logs, queries the
|
|
6
|
+
semantic memory cache for proven patches, and orchestrates targeted re-tests.
|
|
7
|
+
Dispatched automatically by AI Agents when test runs encounter failures.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import re
|
|
13
|
+
import json
|
|
14
|
+
import argparse
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
18
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
19
|
+
try:
|
|
20
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
21
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
27
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
28
|
+
|
|
29
|
+
import pinpoint_failure
|
|
30
|
+
import cache_resolver
|
|
31
|
+
import project_state
|
|
32
|
+
import heuristic_locator_matcher
|
|
33
|
+
|
|
34
|
+
GREEN = "\033[92m"
|
|
35
|
+
RED = "\033[91m"
|
|
36
|
+
YELLOW = "\033[93m"
|
|
37
|
+
CYAN = "\033[96m"
|
|
38
|
+
BOLD = "\033[1m"
|
|
39
|
+
RESET = "\033[0m"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def diagnose_and_plan_heal(target_dir, junit_xml_path=None):
|
|
43
|
+
"""
|
|
44
|
+
Parses test failure, checks memory cache, updates project state watchpoints,
|
|
45
|
+
and returns a structured healing plan.
|
|
46
|
+
"""
|
|
47
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
48
|
+
print(f"{BOLD}{CYAN} AUTONOMOUS CLOSED-LOOP TEST HEALING SENTINEL {RESET}")
|
|
49
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
50
|
+
print(f"📁 Target Project : {target_dir}")
|
|
51
|
+
|
|
52
|
+
# 1. Pinpoint failure from JUnit XML or logs
|
|
53
|
+
failure_info = None
|
|
54
|
+
if junit_xml_path and os.path.exists(junit_xml_path):
|
|
55
|
+
failures = pinpoint_failure.parse_junit_xml(junit_xml_path)
|
|
56
|
+
if failures:
|
|
57
|
+
failure_info = failures[0]
|
|
58
|
+
|
|
59
|
+
if not failure_info:
|
|
60
|
+
# Search for any recent report in Reports/
|
|
61
|
+
reports_dir = os.path.join(target_dir, "Reports")
|
|
62
|
+
if os.path.exists(reports_dir):
|
|
63
|
+
for root, _, files in os.walk(reports_dir):
|
|
64
|
+
for f in files:
|
|
65
|
+
if f.endswith(".xml"):
|
|
66
|
+
parsed = pinpoint_failure.parse_junit_xml(os.path.join(root, f))
|
|
67
|
+
if parsed:
|
|
68
|
+
failure_info = parsed[0]
|
|
69
|
+
break
|
|
70
|
+
if failure_info:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if not failure_info:
|
|
74
|
+
print(" [INFO] Zero active JUnit XML failures detected on disk.")
|
|
75
|
+
return {"status": "NO_FAILURE_DETECTED"}
|
|
76
|
+
|
|
77
|
+
err_sig = failure_info.get("message", failure_info.get("type", "UnknownError"))
|
|
78
|
+
test_name = failure_info.get("name", "UnknownTest")
|
|
79
|
+
failing_file = failure_info.get("file", "UnknownFile")
|
|
80
|
+
line_num = failure_info.get("line", "?")
|
|
81
|
+
|
|
82
|
+
print(f" ❌ Failing Test : {test_name}")
|
|
83
|
+
print(f" 📍 Source Location: {failing_file}:{line_num}")
|
|
84
|
+
print(f" ⚠️ Error Signature: {err_sig[:120]}...\n")
|
|
85
|
+
|
|
86
|
+
# 2. Query 0-token memory cache & 3-layer heuristic matcher
|
|
87
|
+
print(f"{BOLD}[1/2] Checking 3-Layer Heuristic Matcher & Memory Cache...{RESET}")
|
|
88
|
+
cache_solution = cache_resolver.query_cache(err_sig, threshold=0.35)
|
|
89
|
+
|
|
90
|
+
# 2b. Query Episodic Reflexion Memory (Top 2 historical lessons)
|
|
91
|
+
lessons = cache_resolver.query_lessons(f"{test_name} {err_sig}", limit=2)
|
|
92
|
+
|
|
93
|
+
plan = {
|
|
94
|
+
"status": "HEALING_PLAN_READY",
|
|
95
|
+
"test_name": test_name,
|
|
96
|
+
"failing_file": failing_file,
|
|
97
|
+
"line": line_num,
|
|
98
|
+
"error_signature": err_sig,
|
|
99
|
+
"cache_match": bool(cache_solution),
|
|
100
|
+
"heuristic_match": None,
|
|
101
|
+
"recommended_patch": None,
|
|
102
|
+
"episodic_lessons": [l["lesson_summary"] for l in lessons]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if lessons:
|
|
106
|
+
print(f" {CYAN}🧠 [EPISODIC REFLEXION] Top Historical Lessons for Context:{RESET}")
|
|
107
|
+
for idx, l in enumerate(lessons, 1):
|
|
108
|
+
print(f" {idx}. [{l['page_context']}]: {l['lesson_summary']}")
|
|
109
|
+
|
|
110
|
+
if cache_solution:
|
|
111
|
+
print(f" {GREEN}[MATCH] Found cached resolution (Score: {cache_solution['score']:.2f}):{RESET}")
|
|
112
|
+
print(f" 💡 Prescribed Fix : {cache_solution['solution']}")
|
|
113
|
+
plan["recommended_patch"] = cache_solution["solution"]
|
|
114
|
+
else:
|
|
115
|
+
# Check if error message contains broken locator pattern
|
|
116
|
+
loc_candidate_m = re.search(r"locator\(['\"]([^'\"]+)['\"]\)|By\.[a-zA-Z]+\(['\"]([^'\"]+)['\"]\)", err_sig)
|
|
117
|
+
if loc_candidate_m:
|
|
118
|
+
broken_loc = loc_candidate_m.group(1) or loc_candidate_m.group(2)
|
|
119
|
+
heuristic_res = heuristic_locator_matcher.HeuristicLocatorMatcher.match(broken_loc, dom_snapshot=[])
|
|
120
|
+
plan["heuristic_match"] = heuristic_res
|
|
121
|
+
if heuristic_res.get("matched"):
|
|
122
|
+
print(f" {GREEN}[HEURISTIC L{heuristic_res['layer']}] 0-Token match ({heuristic_res['confidence']*100:.1f}%): {heuristic_res['healed_locator']}{RESET}")
|
|
123
|
+
plan["recommended_patch"] = f"Update locator to: {heuristic_res['healed_locator']}"
|
|
124
|
+
else:
|
|
125
|
+
print(" [MISS] Heuristic confidence < 85%. Fallback to Live DOM exploration.")
|
|
126
|
+
plan["recommended_patch"] = "Inspect element using Playwright Live Explorer and apply resilient locator / smart wait."
|
|
127
|
+
else:
|
|
128
|
+
print(" [MISS] No direct cache match. AI Agent will inspect live DOM/AOM via Playwright.")
|
|
129
|
+
plan["recommended_patch"] = "Inspect element using Playwright Live Explorer and apply resilient locator / smart wait."
|
|
130
|
+
|
|
131
|
+
# 3. Anchor watchpoint in PROJECT_STATE.md
|
|
132
|
+
print(f"\n{BOLD}[2/2] Anchoring Watchpoint in PROJECT_STATE.md...{RESET}")
|
|
133
|
+
try:
|
|
134
|
+
project_state.update_state(
|
|
135
|
+
target_dir=target_dir,
|
|
136
|
+
watchpoint=f"{test_name} -> {failing_file}:{line_num}",
|
|
137
|
+
gate_status="HEALING_IN_PROGRESS"
|
|
138
|
+
)
|
|
139
|
+
print(" [OK] Project state watchpoint updated.")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f" ⚠️ Could not update PROJECT_STATE.md: {e}")
|
|
142
|
+
|
|
143
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
144
|
+
return plan
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def record_heal_lesson(page_context, error_signature, root_cause, failed_attempt, successful_fix, lesson_summary):
|
|
148
|
+
"""Saves a 2-line practical lesson learned into SQLite after successful healing."""
|
|
149
|
+
return cache_resolver.record_lesson(
|
|
150
|
+
page_context=page_context,
|
|
151
|
+
error_signature=error_signature,
|
|
152
|
+
root_cause=root_cause,
|
|
153
|
+
failed_attempt=failed_attempt,
|
|
154
|
+
successful_fix=successful_fix,
|
|
155
|
+
lesson_summary=lesson_summary
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
parser = argparse.ArgumentParser(description="Autonomous Closed-Loop Test Healer")
|
|
161
|
+
parser.add_argument("target", nargs="?", default=os.getcwd(), help="Target project root directory")
|
|
162
|
+
parser.add_argument("--xml", default=None, help="Path to JUnit XML test report")
|
|
163
|
+
parser.add_argument("--record-lesson", default=None, help="Record a 2-line lesson learned after healing")
|
|
164
|
+
parser.add_argument("--context", default="GLOBAL", help="Page context for the recorded lesson")
|
|
165
|
+
parser.add_argument("--error", default="UnknownError", help="Error signature for the recorded lesson")
|
|
166
|
+
parser.add_argument("--fix", default="", help="Successful fix for the recorded lesson")
|
|
167
|
+
args = parser.parse_args()
|
|
168
|
+
|
|
169
|
+
if args.record_lesson:
|
|
170
|
+
ok = record_heal_lesson(
|
|
171
|
+
page_context=args.context,
|
|
172
|
+
error_signature=args.error,
|
|
173
|
+
root_cause="Autonomous Healer Diagnosis",
|
|
174
|
+
failed_attempt="",
|
|
175
|
+
successful_fix=args.fix,
|
|
176
|
+
lesson_summary=args.record_lesson
|
|
177
|
+
)
|
|
178
|
+
if ok:
|
|
179
|
+
print(f"✅ [EPISODIC REFLEXION] Lesson recorded for '{args.context}': {args.record_lesson}")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
target_dir = os.path.abspath(args.target)
|
|
183
|
+
plan = diagnose_and_plan_heal(target_dir, junit_xml_path=args.xml)
|
|
184
|
+
print(json.dumps(plan, indent=2))
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
sys.exit(main())
|