synapse-hub 2.1.1 → 2.2.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.
Files changed (58) hide show
  1. package/.agents/agents/orchestrator.md +7 -0
  2. package/.agents/rules/lessons-learned.md +1 -0
  3. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +1 -1
  4. package/.agents/skills/antigravity-test-sentinel/SKILL.md +1 -2
  5. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  6. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +11 -4
  7. package/.agents/skills/architecture-decision-records/SKILL.md +1 -1
  8. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  9. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +32 -11
  10. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  11. package/.agents/skills/clean-pom-crafter/SKILL.md +1 -4
  12. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +1 -1
  13. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +1 -1
  14. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  15. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +1 -3
  16. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  17. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +1 -1
  18. package/.agents/skills/github-actions-templates/SKILL.md +1 -1
  19. package/.agents/skills/graphify-impact-analyzer/SKILL.md +1 -3
  20. package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
  21. package/.agents/skills/lam-script-generator/SKILL.md +1 -2
  22. package/.agents/skills/playwright-live-interaction/SKILL.md +1 -4
  23. package/.agents/skills/security-dast-sentinel/SKILL.md +1 -1
  24. package/.agents/skills/self-healing-locator/SKILL.md +1 -2
  25. package/.agents/skills/skillspector-security-guard/SKILL.md +115 -0
  26. package/.agents/skills/synthetic-test-data-generator/SKILL.md +1 -1
  27. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  28. package/.agents/skills/visual-regression-sentinel/SKILL.md +1 -1
  29. package/.agents/skills/wcag-audit-patterns/SKILL.md +1 -1
  30. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  31. package/AGENTS.md +36 -0
  32. package/README.md +140 -103
  33. package/package.json +1 -1
  34. package/skills_manifest.json +42 -29
  35. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +15 -1
  36. package/templates/ai_bridges/CLAUDE.md +31 -0
  37. package/templates/ai_bridges/copilot-instructions.md +25 -0
  38. package/templates/ai_bridges/cursor_synapse_qa.mdc +26 -0
  39. package/templates/ai_bridges/windsurfrules +25 -0
  40. package/templates/ci/github-actions-synapse.yml +28 -0
  41. package/templates/ci/gitlab-ci-synapse.yml +16 -0
  42. package/tools/TOOLS_RUNBOOK.md +203 -12
  43. package/tools/browser_helpers.js +2 -2
  44. package/tools/bump_version.py +8 -0
  45. package/tools/cli.py +74 -2
  46. package/tools/export_postman_collection.py +535 -218
  47. package/tools/generate_jmx_plan.py +672 -102
  48. package/tools/git_pre_commit_guard.py +21 -0
  49. package/tools/jtl_analyzer.py +474 -0
  50. package/tools/mcp_excel_server.ps1 +1 -1
  51. package/tools/mcp_server.py +1 -1
  52. package/tools/newman_analyzer.py +385 -0
  53. package/tools/setup_project.py +203 -18
  54. package/tools/skillspector.py +1141 -0
  55. package/tools/telemetry_correlator.py +351 -0
  56. package/tools/templates/jmeter-distributed/README.md +54 -0
  57. package/tools/templates/jmeter-distributed/docker-compose.yml +83 -0
  58. package/tools/update_sentinel.py +313 -0
@@ -13,6 +13,14 @@ import sys
13
13
  import os
14
14
  import subprocess
15
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
+
16
24
  FORBIDDEN_PATHS = ["/Users/", "C:\\Workspace\\", "D:\\Workspace\\", "file:///c:", "file:///d:"]
17
25
 
18
26
  def run_cmd(cmd):
@@ -135,6 +143,19 @@ def main():
135
143
 
136
144
  errors_found = False
137
145
 
146
+ # 0. Hub Pollution Guard (When committing inside Synapse-Hub)
147
+ is_hub = os.path.exists("skills_manifest.json") and os.path.isdir(".agents")
148
+ if is_hub:
149
+ forbidden_hub_prefixes = ("Scripts/", "Keywords/", "Object Repository/", "Test Suites/", "Data Files/", "Include/")
150
+ polluting_files = [f for f in staged_files if any(f.startswith(p) for p in forbidden_hub_prefixes)]
151
+ if polluting_files:
152
+ print("āŒ [PRE-COMMIT BLOCKED] Target project business test files detected in Synapse-Hub:")
153
+ for pf in polluting_files:
154
+ print(f" - {pf}")
155
+ print("\n🚨 CWD CONFUSION GUARD: You are attempting to commit target project code into Synapse-Hub!")
156
+ print(" Please unstage these files ('git reset HEAD') and commit them inside your target project workspace.")
157
+ errors_found = True
158
+
138
159
  # 1. BOM Check
139
160
  target_exts = (".groovy", ".tc", ".ts", ".glbl", ".md", ".py", ".json", ".ps1")
140
161
  check_files = [f for f in staged_files if f.endswith(target_exts)]
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ tools/jtl_analyzer.py — Enterprise Apache JMeter JTL Results Analyzer & CI/CD Gatekeeper
5
+ -----------------------------------------------------------------------------------------
6
+ Parses Apache JMeter JTL log results (CSV or XML format), calculates statistical percentiles
7
+ (P50, P90, P95, P99, Throughput, Error Rate), evaluates SLA compliance thresholds, and
8
+ enforces zero-tolerance Quality Gates in CI/CD pipelines with deterministic exit codes.
9
+
10
+ Features:
11
+ 1. Universal Parsing: Supports standard JMeter CSV JTL and legacy XML JTL formats.
12
+ 2. Statistical Engine: Calculates Min, Max, Average, P50 (Median), P90, P95, P99,
13
+ Throughput (TPS/RPS), Network KB/s, and Error Rate (%) per sampler and aggregate.
14
+ 3. CI/CD Quality Gate: Evaluates user-defined SLA thresholds:
15
+ - Maximum acceptable P95 / P99 latency (ms)
16
+ - Maximum acceptable Average latency (ms)
17
+ - Maximum acceptable Error Rate (%)
18
+ Returns Exit Code 0 on PASS, Exit Code 1 on SLA breach.
19
+ 4. Multi-Format Reporting:
20
+ - ANSI terminal summary table
21
+ - GitHub / GitLab Markdown report for Pull Request annotations
22
+ - Raw JSON export for downstream telemetry and APM indexing.
23
+
24
+ Usage:
25
+ python tools/jtl_analyzer.py results.jtl --sla-p95 500 --max-error-rate 1.0
26
+ python tools/jtl_analyzer.py results.jtl --markdown report.md --json metrics.json
27
+ agent-qa analyze-jtl results.jtl --sla-p95 400
28
+ """
29
+
30
+ import os
31
+ import sys
32
+ import csv
33
+ import json
34
+ import math
35
+ import argparse
36
+ import xml.etree.ElementTree as ET
37
+ from typing import Dict, List, Any, Optional, Tuple
38
+
39
+ # Enforce UTF-8 stdout encoding for Windows/macOS/Linux console compatibility
40
+ if hasattr(sys.stdout, "reconfigure"):
41
+ try:
42
+ sys.stdout.reconfigure(encoding="utf-8")
43
+ sys.stderr.reconfigure(encoding="utf-8")
44
+ except Exception:
45
+ pass
46
+
47
+
48
+ def calculate_percentile(sorted_values: List[float], percentile: float) -> float:
49
+ """Calculates percentile from a pre-sorted list using linear interpolation."""
50
+ if not sorted_values:
51
+ return 0.0
52
+ k = (len(sorted_values) - 1) * (percentile / 100.0)
53
+ f = math.floor(k)
54
+ c = math.ceil(k)
55
+ if f == c:
56
+ return float(sorted_values[int(k)])
57
+ d0 = sorted_values[int(f)] * (c - k)
58
+ d1 = sorted_values[int(c)] * (k - f)
59
+ return float(d0 + d1)
60
+
61
+
62
+ class SamplerStats:
63
+ """Aggregates metrics for a single sampler label."""
64
+ def __init__(self, label: str):
65
+ self.label = label
66
+ self.samples: int = 0
67
+ self.errors: int = 0
68
+ self.elapsed_times: List[float] = []
69
+ self.total_bytes: int = 0
70
+ self.start_time: Optional[int] = None
71
+ self.end_time: Optional[int] = None
72
+ self.response_codes: Dict[str, int] = {}
73
+
74
+ def add_sample(self, elapsed: float, success: bool, timestamp: int, bytes_recv: int, response_code: str):
75
+ self.samples += 1
76
+ if not success:
77
+ self.errors += 1
78
+ self.elapsed_times.append(elapsed)
79
+ self.total_bytes += bytes_recv
80
+
81
+ if self.start_time is None or timestamp < self.start_time:
82
+ self.start_time = timestamp
83
+ if self.end_time is None or timestamp > self.end_time:
84
+ self.end_time = timestamp
85
+
86
+ code = str(response_code).strip() or "UNKNOWN"
87
+ self.response_codes[code] = self.response_codes.get(code, 0) + 1
88
+
89
+ def compute(self) -> Dict[str, Any]:
90
+ if not self.elapsed_times:
91
+ return {
92
+ "label": self.label,
93
+ "samples": 0,
94
+ "errors": 0,
95
+ "error_rate_pct": 0.0,
96
+ "avg_ms": 0.0,
97
+ "min_ms": 0.0,
98
+ "max_ms": 0.0,
99
+ "median_ms": 0.0,
100
+ "p90_ms": 0.0,
101
+ "p95_ms": 0.0,
102
+ "p99_ms": 0.0,
103
+ "throughput_tps": 0.0,
104
+ "kb_per_sec": 0.0,
105
+ "response_codes": self.response_codes,
106
+ }
107
+
108
+ sorted_times = sorted(self.elapsed_times)
109
+ total_time = sum(sorted_times)
110
+ count = len(sorted_times)
111
+ avg_ms = total_time / count
112
+ min_ms = sorted_times[0]
113
+ max_ms = sorted_times[-1]
114
+ p50 = calculate_percentile(sorted_times, 50.0)
115
+ p90 = calculate_percentile(sorted_times, 90.0)
116
+ p95 = calculate_percentile(sorted_times, 95.0)
117
+ p99 = calculate_percentile(sorted_times, 99.0)
118
+
119
+ error_rate = (self.errors / count) * 100.0 if count > 0 else 0.0
120
+
121
+ # Duration in seconds
122
+ duration_sec = 0.0
123
+ if self.start_time is not None and self.end_time is not None:
124
+ duration_sec = max((self.end_time - self.start_time) / 1000.0, 0.001)
125
+
126
+ tps = count / duration_sec if duration_sec > 0 else 0.0
127
+ kb_sec = (self.total_bytes / 1024.0) / duration_sec if duration_sec > 0 else 0.0
128
+
129
+ return {
130
+ "label": self.label,
131
+ "samples": count,
132
+ "errors": self.errors,
133
+ "error_rate_pct": round(error_rate, 2),
134
+ "avg_ms": round(avg_ms, 2),
135
+ "min_ms": round(min_ms, 2),
136
+ "max_ms": round(max_ms, 2),
137
+ "median_ms": round(p50, 2),
138
+ "p90_ms": round(p90, 2),
139
+ "p95_ms": round(p95, 2),
140
+ "p99_ms": round(p99, 2),
141
+ "throughput_tps": round(tps, 2),
142
+ "kb_per_sec": round(kb_sec, 2),
143
+ "response_codes": self.response_codes,
144
+ }
145
+
146
+
147
+ def parse_csv_jtl(filepath: str) -> Tuple[Dict[str, SamplerStats], SamplerStats]:
148
+ """Parses standard comma/tab delimited JTL CSV file."""
149
+ per_sampler: Dict[str, SamplerStats] = {}
150
+ aggregate = SamplerStats("TOTAL AGGREGATE")
151
+
152
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
153
+ # Detect delimiter
154
+ first_line = f.readline()
155
+ if not first_line:
156
+ return per_sampler, aggregate
157
+ delimiter = "\t" if "\t" in first_line else ","
158
+ f.seek(0)
159
+
160
+ reader = csv.DictReader(f, delimiter=delimiter)
161
+ for row in reader:
162
+ try:
163
+ # Column names can vary slightly across JMeter versions
164
+ elapsed = float(row.get("elapsed") or row.get("Elapsed") or row.get("time") or 0)
165
+ label = row.get("label") or row.get("Label") or "Unknown"
166
+
167
+ success_val = row.get("success") or row.get("Success") or "true"
168
+ success = str(success_val).strip().lower() in ("true", "1")
169
+
170
+ ts_val = row.get("timeStamp") or row.get("Timestamp") or row.get("t") or 0
171
+ timestamp = int(float(ts_val))
172
+
173
+ bytes_val = row.get("bytes") or row.get("Bytes") or 0
174
+ bytes_recv = int(float(bytes_val))
175
+
176
+ resp_code = row.get("responseCode") or row.get("ResponseCode") or row.get("rc") or "200"
177
+
178
+ if label not in per_sampler:
179
+ per_sampler[label] = SamplerStats(label)
180
+ per_sampler[label].add_sample(elapsed, success, timestamp, bytes_recv, resp_code)
181
+ aggregate.add_sample(elapsed, success, timestamp, bytes_recv, resp_code)
182
+ except Exception:
183
+ continue
184
+
185
+ return per_sampler, aggregate
186
+
187
+
188
+ def parse_xml_jtl(filepath: str) -> Tuple[Dict[str, SamplerStats], SamplerStats]:
189
+ """Parses legacy XML JTL file (<testResults> / <httpSample> / <sample>)."""
190
+ per_sampler: Dict[str, SamplerStats] = {}
191
+ aggregate = SamplerStats("TOTAL AGGREGATE")
192
+
193
+ try:
194
+ tree = ET.parse(filepath)
195
+ root = tree.getroot()
196
+ for elem in root.findall(".//httpSample") + root.findall(".//sample"):
197
+ try:
198
+ elapsed = float(elem.attrib.get("t", 0))
199
+ label = elem.attrib.get("lb", "Unknown")
200
+ success = elem.attrib.get("s", "true").lower() == "true"
201
+ timestamp = int(elem.attrib.get("ts", 0))
202
+ bytes_recv = int(elem.attrib.get("by", 0))
203
+ resp_code = elem.attrib.get("rc", "200")
204
+
205
+ if label not in per_sampler:
206
+ per_sampler[label] = SamplerStats(label)
207
+ per_sampler[label].add_sample(elapsed, success, timestamp, bytes_recv, resp_code)
208
+ aggregate.add_sample(elapsed, success, timestamp, bytes_recv, resp_code)
209
+ except Exception:
210
+ continue
211
+ except Exception as e:
212
+ print(f"āš ļø XML Parse Error: {e}", file=sys.stderr)
213
+
214
+ return per_sampler, aggregate
215
+
216
+
217
+ def parse_jtl(filepath: str) -> Tuple[Dict[str, SamplerStats], SamplerStats]:
218
+ """Detects format and parses JTL file."""
219
+ if not os.path.exists(filepath):
220
+ raise FileNotFoundError(f"JTL file not found: {filepath}")
221
+
222
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
223
+ head = f.read(256).strip()
224
+
225
+ if head.startswith("<?xml") or head.startswith("<testResults"):
226
+ return parse_xml_jtl(filepath)
227
+ return parse_csv_jtl(filepath)
228
+
229
+
230
+ def evaluate_sla(
231
+ agg_stats: Dict[str, Any],
232
+ sla_p95: Optional[float] = None,
233
+ sla_p99: Optional[float] = None,
234
+ sla_avg: Optional[float] = None,
235
+ max_error_rate: Optional[float] = None
236
+ ) -> Tuple[bool, List[str]]:
237
+ """Evaluates computed aggregate metrics against SLA limits."""
238
+ violations: List[str] = []
239
+
240
+ if sla_p95 is not None and agg_stats["p95_ms"] > sla_p95:
241
+ violations.append(f"P95 Latency SLA Breached: {agg_stats['p95_ms']}ms > {sla_p95}ms")
242
+
243
+ if sla_p99 is not None and agg_stats["p99_ms"] > sla_p99:
244
+ violations.append(f"P99 Latency SLA Breached: {agg_stats['p99_ms']}ms > {sla_p99}ms")
245
+
246
+ if sla_avg is not None and agg_stats["avg_ms"] > sla_avg:
247
+ violations.append(f"Average Latency SLA Breached: {agg_stats['avg_ms']}ms > {sla_avg}ms")
248
+
249
+ if max_error_rate is not None and agg_stats["error_rate_pct"] > max_error_rate:
250
+ violations.append(f"Error Rate SLA Breached: {agg_stats['error_rate_pct']}% > {max_error_rate}%")
251
+
252
+ passed = (len(violations) == 0)
253
+ return passed, violations
254
+
255
+
256
+ def format_table(per_sampler_data: List[Dict[str, Any]], agg_data: Dict[str, Any]) -> str:
257
+ """Formats metrics into a clean CLI table."""
258
+ headers = ["Sampler Label", "Samples", "Errors (%)", "Avg (ms)", "Min", "Max", "P90", "P95", "P99", "TPS"]
259
+ col_w = [30, 9, 12, 10, 8, 8, 8, 8, 8, 10]
260
+
261
+ lines = []
262
+ header_str = " | ".join(h.ljust(col_w[i]) for i, h in enumerate(headers))
263
+ sep_str = "-+-".join("-" * col_w[i] for i in range(len(headers)))
264
+
265
+ lines.append(header_str)
266
+ lines.append(sep_str)
267
+
268
+ all_rows = list(per_sampler_data) + [agg_data]
269
+ for row in all_rows:
270
+ label = row["label"]
271
+ if len(label) > 28:
272
+ label = label[:25] + "..."
273
+ err_str = f"{row['errors']} ({row['error_rate_pct']}%)"
274
+
275
+ row_str = " | ".join([
276
+ label.ljust(col_w[0]),
277
+ str(row["samples"]).rjust(col_w[1]),
278
+ err_str.rjust(col_w[2]),
279
+ f"{row['avg_ms']:.1f}".rjust(col_w[3]),
280
+ f"{row['min_ms']:.0f}".rjust(col_w[4]),
281
+ f"{row['max_ms']:.0f}".rjust(col_w[5]),
282
+ f"{row['p90_ms']:.1f}".rjust(col_w[6]),
283
+ f"{row['p95_ms']:.1f}".rjust(col_w[7]),
284
+ f"{row['p99_ms']:.1f}".rjust(col_w[8]),
285
+ f"{row['throughput_tps']:.1f}".rjust(col_w[9]),
286
+ ])
287
+ if row == agg_data:
288
+ lines.append(sep_str)
289
+ lines.append(row_str)
290
+
291
+ return "\n".join(lines)
292
+
293
+
294
+ def generate_markdown_report(
295
+ jtl_file: str,
296
+ per_sampler_data: List[Dict[str, Any]],
297
+ agg_data: Dict[str, Any],
298
+ passed: bool,
299
+ violations: List[str],
300
+ sla_info: Dict[str, Any]
301
+ ) -> str:
302
+ """Generates GitHub / GitLab Markdown report for Pull Request annotations."""
303
+ status_badge = "āœ… **PASSED**" if passed else "āŒ **FAILED (SLA BREACHED)**"
304
+
305
+ lines = [
306
+ "# šŸš€ Apache JMeter Performance & Load Test Report",
307
+ "",
308
+ f"- **Source Result File:** `{os.path.basename(jtl_file)}`",
309
+ f"- **Quality Gate Status:** {status_badge}",
310
+ f"- **Total Virtual Requests:** `{agg_data['samples']:,}`",
311
+ f"- **Total Errors:** `{agg_data['errors']}` ({agg_data['error_rate_pct']}%)",
312
+ f"- **Average Latency:** `{agg_data['avg_ms']} ms`",
313
+ f"- **95th Percentile (P95):** `{agg_data['p95_ms']} ms`",
314
+ f"- **Throughput:** `{agg_data['throughput_tps']} req/sec`",
315
+ "",
316
+ ]
317
+
318
+ if violations:
319
+ lines.extend([
320
+ "### āš ļø SLA Violations Detected",
321
+ "",
322
+ ])
323
+ for v in violations:
324
+ lines.append(f"- šŸ”“ `{v}`")
325
+ lines.append("")
326
+
327
+ lines.extend([
328
+ "### šŸ“Š Detailed Sampler Breakdown",
329
+ "",
330
+ "| Sampler Label | Samples | Errors | Error % | Avg (ms) | Min (ms) | Max (ms) | P90 (ms) | P95 (ms) | P99 (ms) | TPS |",
331
+ "| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |",
332
+ ])
333
+
334
+ for row in per_sampler_data:
335
+ lines.append(
336
+ f"| `{row['label']}` | {row['samples']:,} | {row['errors']} | {row['error_rate_pct']}% | "
337
+ f"{row['avg_ms']} | {row['min_ms']} | {row['max_ms']} | {row['p90_ms']} | "
338
+ f"**{row['p95_ms']}** | {row['p99_ms']} | {row['throughput_tps']} |"
339
+ )
340
+
341
+ # Aggregate row
342
+ lines.append(
343
+ f"| **TOTAL AGGREGATE** | **{agg_data['samples']:,}** | **{agg_data['errors']}** | **{agg_data['error_rate_pct']}%** | "
344
+ f"**{agg_data['avg_ms']}** | **{agg_data['min_ms']}** | **{agg_data['max_ms']}** | **{agg_data['p90_ms']}** | "
345
+ f"**{agg_data['p95_ms']}** | **{agg_data['p99_ms']}** | **{agg_data['throughput_tps']}** |"
346
+ )
347
+
348
+ lines.extend([
349
+ "",
350
+ "---",
351
+ "*Report auto-generated by Synapse-Hub `jtl_analyzer.py` - Enterprise Performance Quality Gate.*"
352
+ ])
353
+
354
+ return "\n".join(lines)
355
+
356
+
357
+ def main():
358
+ parser = argparse.ArgumentParser(
359
+ description="Apache JMeter JTL Results Analyzer & CI/CD SLA Quality Gatekeeper."
360
+ )
361
+ parser.add_argument("jtl_file", nargs="?", help="Path to JMeter results file (.jtl or .csv).")
362
+ parser.add_argument("--sla-p95", type=float, default=None, help="P95 Latency threshold in ms (fails if exceeded).")
363
+ parser.add_argument("--sla-p99", type=float, default=None, help="P99 Latency threshold in ms (fails if exceeded).")
364
+ parser.add_argument("--sla-avg", type=float, default=None, help="Average Latency threshold in ms (fails if exceeded).")
365
+ parser.add_argument("--max-error-rate", type=float, default=None, help="Maximum allowed error rate percentage (e.g. 1.0 = 1%).")
366
+ parser.add_argument("--markdown", "-m", help="Output path for Markdown summary report.")
367
+ parser.add_argument("--json", "-j", help="Output path for JSON summary metrics.")
368
+ parser.add_argument("--telemetry", help="Path to server telemetry CSV (timestamp, cpu_pct, memory_pct, db_pool_pct) for Full-Stack APM correlation.")
369
+ parser.add_argument("--sample", action="store_true", help="Generate and analyze a mock JTL dataset for verification.")
370
+
371
+ args = parser.parse_args()
372
+
373
+ # Handle sample verification mode
374
+ if args.sample or not args.jtl_file:
375
+ import tempfile
376
+ temp_jtl = os.path.join(tempfile.gettempdir(), "sample_jmeter_run.jtl")
377
+ with open(temp_jtl, "w", encoding="utf-8", newline="") as f:
378
+ writer = csv.writer(f)
379
+ writer.writerow(["timeStamp", "elapsed", "label", "responseCode", "responseMessage", "threadName", "dataType", "success", "failureMessage", "bytes", "sentBytes", "grpThreads", "allThreads", "URL", "Latency", "IdleTime", "Connect"])
380
+
381
+ # Generate 200 synthetic rows
382
+ import time
383
+ base_time = int(time.time() * 1000)
384
+ for i in range(150):
385
+ writer.writerow([base_time + (i * 100), 120 + (i % 50), "[GET] /api/v1/catalogs", "200", "OK", "Thread 1-1", "text", "true", "", 1024, 256, 10, 10, "http://api.local/catalogs", 115, 0, 15])
386
+ for i in range(45):
387
+ writer.writerow([base_time + (i * 100), 280 + (i % 80), "[POST] /api/v1/orders", "201", "Created", "Thread 1-2", "text", "true", "", 2048, 512, 10, 10, "http://api.local/orders", 270, 0, 20])
388
+ for i in range(5):
389
+ writer.writerow([base_time + (i * 100), 1500, "[POST] /api/v1/orders", "500", "Internal Error", "Thread 1-2", "text", "false", "DB Timeout", 512, 512, 10, 10, "http://api.local/orders", 1490, 0, 25])
390
+
391
+ args.jtl_file = temp_jtl
392
+ print(f"šŸ”¬ [SAMPLE MODE] Generated synthetic benchmark JTL: {temp_jtl}")
393
+
394
+ per_sampler_raw, agg_raw = parse_jtl(args.jtl_file)
395
+ if not per_sampler_raw:
396
+ print(f"āš ļø No sample records found in {args.jtl_file}", file=sys.stderr)
397
+ sys.exit(0)
398
+
399
+ per_sampler_data = [stats.compute() for stats in per_sampler_raw.values()]
400
+ agg_data = agg_raw.compute()
401
+
402
+ passed, violations = evaluate_sla(
403
+ agg_data,
404
+ sla_p95=args.sla_p95,
405
+ sla_p99=args.sla_p99,
406
+ sla_avg=args.sla_avg,
407
+ max_error_rate=args.max_error_rate
408
+ )
409
+
410
+ print("\n" + "=" * 90)
411
+ print(" APACHE JMETER PERFORMANCE RESULTS MATRIX (CI/CD QUALITY GATE)")
412
+ print("=" * 90)
413
+ print(format_table(per_sampler_data, agg_data))
414
+ print("=" * 90)
415
+
416
+ # Print SLA Verdict
417
+ print(f"\nšŸŽÆ [QUALITY GATE SLA VERDICT]")
418
+ if passed:
419
+ print("āœ… ALL PERFORMANCE SLAs PASSED! Ready for deployment/merge.")
420
+ else:
421
+ print("āŒ PERFORMANCE SLA BREACHED:")
422
+ for v in violations:
423
+ print(f" šŸ”“ {v}")
424
+
425
+ # Optional Telemetry Correlation
426
+ telemetry_analysis = None
427
+ if args.telemetry and os.path.exists(args.telemetry):
428
+ from telemetry_correlator import parse_jtl_time_series, parse_server_metrics, run_correlation_analysis
429
+ jtl_buckets = parse_jtl_time_series(args.jtl_file, bucket_sec=5)
430
+ server_series = parse_server_metrics(args.telemetry, bucket_sec=5)
431
+ telemetry_analysis = run_correlation_analysis(jtl_buckets, server_series)
432
+ print("\n" + "=" * 90)
433
+ print(" FULL-STACK APM & SERVER TELEMETRY CORRELATION (RCA)")
434
+ print("=" * 90)
435
+ for k, v in telemetry_analysis["correlations"].items():
436
+ print(f" • {k.replace('_', ' ').title():<25} : {v:+0.3f}")
437
+ for c in telemetry_analysis["root_causes"]:
438
+ print(f" [{c['severity']}] {c['component']}: {c['description']}")
439
+ print(f" šŸ‘‰ Recommendation: {c['recommendation']}")
440
+
441
+ # Export Markdown
442
+ if args.markdown:
443
+ md_content = generate_markdown_report(
444
+ args.jtl_file, per_sampler_data, agg_data, passed, violations, vars(args)
445
+ )
446
+ if telemetry_analysis:
447
+ from telemetry_correlator import format_rca_markdown
448
+ md_content += "\n\n" + format_rca_markdown(telemetry_analysis)
449
+ os.makedirs(os.path.dirname(os.path.abspath(args.markdown)) or ".", exist_ok=True)
450
+ with open(args.markdown, "w", encoding="utf-8") as f:
451
+ f.write(md_content)
452
+ print(f"šŸ“„ Markdown Report exported: {args.markdown}")
453
+
454
+ # Export JSON
455
+ if args.json:
456
+ export_payload = {
457
+ "source_jtl": os.path.abspath(args.jtl_file),
458
+ "passed": passed,
459
+ "violations": violations,
460
+ "aggregate": agg_data,
461
+ "samplers": per_sampler_data,
462
+ "telemetry_correlation": telemetry_analysis
463
+ }
464
+ os.makedirs(os.path.dirname(os.path.abspath(args.json)) or ".", exist_ok=True)
465
+ with open(args.json, "w", encoding="utf-8") as f:
466
+ json.dump(export_payload, f, indent=2)
467
+ print(f"šŸ“Š JSON Metrics exported: {args.json}")
468
+
469
+ # Exit code enforcement
470
+ sys.exit(0 if passed else 1)
471
+
472
+
473
+ if __name__ == "__main__":
474
+ main()
@@ -160,7 +160,7 @@ while (($line = $inputReader.ReadLine()) -ne $null) {
160
160
  result = @{
161
161
  protocolVersion = "2024-11-05"
162
162
  capabilities = @{ tools = @{} }
163
- serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.1.1" }
163
+ serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.2.0" }
164
164
  }
165
165
  }
166
166
  [System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
@@ -43,7 +43,7 @@ import traffic_session_converter
43
43
  import visual_som_helper
44
44
 
45
45
  SERVER_NAME = "enterprise-agent-qa"
46
- SERVER_VERSION = "2.1.1"
46
+ SERVER_VERSION = "2.2.0"
47
47
 
48
48
  MCP_TOOLS = [
49
49
  {