synapse-hub 2.7.0 → 3.0.1

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 (141) hide show
  1. package/.agents/agents/live-explorer.md +2 -0
  2. package/.agents/agents/pom-crafter.md +2 -0
  3. package/.agents/agents/test-runner.md +3 -0
  4. package/.agents/rules/communication-style.md +4 -0
  5. package/.agents/rules/context-engineering.md +1 -0
  6. package/.agents/rules/git-push-protocol.md +17 -0
  7. package/.agents/skills/agentless-qa-repair/examples/three_phase_repair_workflow.md +40 -0
  8. package/.agents/skills/antigravity-test-sentinel/examples/sprint_dod_enforcement.md +47 -0
  9. package/.agents/skills/auth-session-and-cookie-manager/examples/multi_role_auth_matrix.md +70 -0
  10. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_playwright_pom.ts +52 -0
  11. package/.agents/skills/failure-autopsy-and-healing/examples/closed_loop_healing_walkthrough.md +48 -0
  12. package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
  13. package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +1 -1
  14. package/.agents/skills/mipro-prompt-compiler/examples/dspy_assertion_optimizer_case.md +46 -0
  15. package/.agents/skills/persistent-planning-files/SKILL.md +1 -1
  16. package/.agents/skills/qa-aci-interface/examples/surgical_code_window_walkthrough.md +40 -0
  17. package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
  18. package/.agents/skills/review-spec/SKILL.md +1 -1
  19. package/.agents/skills/review-spec/examples/pocock_grilling_decision_rubric.md +40 -0
  20. package/.agents/skills/senior-code-review/SKILL.md +1 -1
  21. package/.agents/skills/skillspector-security-guard/examples/audit_threat_vectors_report.json +35 -0
  22. package/.agents/skills/surgical-coding-principles/SKILL.md +1 -1
  23. package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
  24. package/.agents/skills/tot-exploratory-testing/examples/payment_gateway_tot_tree.json +62 -0
  25. package/.agents/skills/verification-before-completion/SKILL.md +2 -2
  26. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +2 -2
  27. package/.agents/skills/web-agent-gym-benchmarking/examples/enterprise_tasks_suite.json +38 -0
  28. package/.agents/skills/web-vitals-and-performance-profiler/examples/playwright_cwv_profiler_fixture.ts +61 -0
  29. package/AGENTS.md +6 -4
  30. package/CLAUDE.md +2 -1
  31. package/README.en.md +71 -292
  32. package/README.md +69 -304
  33. package/action.yml +1 -1
  34. package/bin/agent-qa.js +2 -2
  35. package/docs/ARCHITECTURE.en.md +192 -0
  36. package/docs/ARCHITECTURE.md +135 -88
  37. package/docs/CI_CD_INTEGRATION.en.md +96 -0
  38. package/docs/CI_CD_INTEGRATION.md +24 -22
  39. package/docs/CLI_REFERENCE.en.md +174 -0
  40. package/docs/CLI_REFERENCE.md +120 -115
  41. package/docs/CONTRIBUTING.en.md +49 -0
  42. package/docs/CONTRIBUTING.md +26 -68
  43. package/docs/QUALITY_GATES.en.md +115 -0
  44. package/docs/QUALITY_GATES.md +115 -0
  45. package/docs/research/agent_skill_training_analysis.md +1 -1
  46. package/package.json +1 -1
  47. package/scripts/package_dist.py +2 -1
  48. package/skills_manifest.json +6 -6
  49. package/synapse.config.json +1 -1
  50. package/templates/ai_bridges/CLAUDE.md +2 -1
  51. package/templates/ai_bridges/copilot-instructions.md +2 -2
  52. package/tools/__init__.py +18 -1
  53. package/tools/_openapi_parser.py +1 -1
  54. package/tools/agent_canary.py +42 -30
  55. package/tools/agentless_patcher.py +2 -2
  56. package/tools/analyze_project.py +3 -5
  57. package/tools/api_drift_sentinel.py +4 -4
  58. package/tools/api_fuzzer.py +8 -8
  59. package/tools/auto_heal.py +4 -5
  60. package/tools/auto_heal_reflexion.py +10 -2
  61. package/tools/autogen_qa_broker.py +2 -2
  62. package/tools/browser_helpers.js +2 -2
  63. package/tools/browsergym_eval.py +2 -3
  64. package/tools/bug_reporter.py +11 -13
  65. package/tools/bump_version.py +90 -19
  66. package/tools/cache_resolver.py +78 -83
  67. package/tools/check_env.py +9 -1
  68. package/tools/check_version_sync.py +58 -4
  69. package/tools/ci_healing_hook.py +4 -6
  70. package/tools/cli.py +5 -7
  71. package/tools/config_decoupler.py +2 -3
  72. package/tools/contract_validator.py +141 -0
  73. package/tools/core/__init__.py +52 -0
  74. package/tools/count_tokens.py +22 -20
  75. package/tools/dashboard_generator.py +629 -1000
  76. package/tools/db_sandbox.py +87 -62
  77. package/tools/debt_harvester.py +3 -3
  78. package/tools/distill_log.py +3 -3
  79. package/tools/dspy_qa_signatures.py +3 -4
  80. package/tools/export_postman_collection.py +8 -8
  81. package/tools/flaky_detector.py +2 -2
  82. package/tools/fuzz_payloads.py +5 -5
  83. package/tools/generate_allure_report.py +1 -1
  84. package/tools/generate_jmx_plan.py +4 -4
  85. package/tools/gherkin_suite.py +1 -1
  86. package/tools/git_pre_commit_guard.py +2 -2
  87. package/tools/goldfish_review.py +2 -2
  88. package/tools/heal/__init__.py +28 -0
  89. package/tools/heal_pr_automator.py +6 -6
  90. package/tools/heuristic_locator_matcher.py +2 -2
  91. package/tools/infer_linter.py +143 -29
  92. package/tools/interview_story.py +0 -1
  93. package/tools/jtl_analyzer.py +11 -11
  94. package/tools/lint/__init__.py +30 -0
  95. package/tools/lint_ai_docs.py +42 -0
  96. package/tools/lookup_tc.py +1 -1
  97. package/tools/matrix_sharder.py +15 -13
  98. package/tools/mcp/__init__.py +16 -0
  99. package/tools/mcp_excel_server.py +2 -2
  100. package/tools/mcp_server.py +9 -7
  101. package/tools/md_to_xlsx.py +26 -26
  102. package/tools/micro_mutator.py +32 -15
  103. package/tools/network_mock_shield.py +27 -27
  104. package/tools/newman_analyzer.py +2 -3
  105. package/tools/omni_emitter.py +5 -8
  106. package/tools/omni_parser.py +5 -5
  107. package/tools/omni_transpiler.py +5 -5
  108. package/tools/output_formatter.py +19 -0
  109. package/tools/perf/__init__.py +24 -0
  110. package/tools/pinpoint_failure.py +1 -1
  111. package/tools/project_state.py +0 -2
  112. package/tools/qa_event_stream.py +1 -1
  113. package/tools/repo_map.py +8 -8
  114. package/tools/report/__init__.py +32 -0
  115. package/tools/reversible_sandbox.py +3 -4
  116. package/tools/safety_firewall.py +2 -2
  117. package/tools/senior_code_review.py +4 -3
  118. package/tools/setup_project.py +10 -12
  119. package/tools/skill_collision_guard.py +3 -3
  120. package/tools/skill_vault.py +94 -6
  121. package/tools/skillspector.py +10 -8
  122. package/tools/smart_visual_masker.py +4 -6
  123. package/tools/spec_drift_sentinel.py +1 -2
  124. package/tools/swe_qa_aci.py +2 -2
  125. package/tools/synapse_qa_bench.py +3 -3
  126. package/tools/sync_agent_rules.py +18 -3
  127. package/tools/telemetry_correlator.py +3 -2
  128. package/tools/test_impact.py +2 -1
  129. package/tools/test_memory_os.py +15 -5
  130. package/tools/textgrad_mutation_optimizer.py +3 -3
  131. package/tools/tms_sync_bridge.py +5 -5
  132. package/tools/tot_exploratory_tester.py +5 -6
  133. package/tools/traffic_session_converter.py +9 -9
  134. package/tools/universal_ir.py +2 -3
  135. package/tools/update_sentinel.py +6 -9
  136. package/tools/verify_all.py +12 -2
  137. package/tools/visual_diff_canvas.py +4 -4
  138. package/tools/visual_som_helper.py +15 -13
  139. package/tools/watch_mode.py +1 -1
  140. package/tools/wcag_audit.py +22 -1
  141. package/tools/welcome_page.py +5 -6
@@ -16,6 +16,7 @@ import sys
16
16
  import re
17
17
  import json
18
18
  import glob
19
+ import ast
19
20
  import html as html_lib
20
21
  import argparse
21
22
  from datetime import datetime
@@ -44,7 +45,7 @@ RESET = "\033[0m"
44
45
 
45
46
  # Optional Tree-Sitter Integration for compiler-grade AST parsing across TS/Java/C#
46
47
  try:
47
- import tree_sitter # type: ignore
48
+ import tree_sitter # type: ignore # noqa: F401
48
49
  HAS_TREE_SITTER = True
49
50
  except ImportError:
50
51
  HAS_TREE_SITTER = False
@@ -76,15 +77,19 @@ def parse_with_tree_sitter(file_path, content):
76
77
  return None
77
78
 
78
79
  # Generic Detection regexes (including Cypress .should and .and assertions)
80
+ # Note: Standalone expect(...) without chained matcher is caught as DANGLING_ASSERTION_NO_MATCHER.
79
81
  ASSERTION_PATTERN = re.compile(
80
- r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b|\.should\s*\(|\.and\s*\('
82
+ r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b'
83
+ r'|\.should\s*\('
84
+ r'|\.and\s*\(',
85
+ re.MULTILINE
81
86
  )
82
87
 
83
88
  # Rule L1 & L5: Empty catch / swallowed exceptions across JS/TS/Groovy/Java and Python
84
- # Also catches try/catch that only console.log or promise .catch(() => {})
89
+ # Also catches try/catch that only console.log/logger.error or promise .catch(() => {})
85
90
  EMPTY_CATCH_PATTERN = re.compile(
86
- r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}'
87
- r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}\s*\)'
91
+ r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}'
92
+ r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}\s*\)'
88
93
  r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
89
94
  re.MULTILINE
90
95
  )
@@ -96,16 +101,48 @@ BENIGN_TRY_CALL_PATTERN = re.compile(
96
101
  r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
97
102
  )
98
103
 
99
- # Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, expect(true).toBeTruthy())
104
+ # Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, assert f() == f(), assert len(x) >= 0)
100
105
  TAUTOLOGY_PATTERN = re.compile(
101
- r'(?:expect\s*\(\s*(?:true|1)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy)\s*\(\s*(?:true|1)?\s*\)'
106
+ r'(?:expect(?:\.soft|\.poll)?\s*\(\s*(?:true|1|null|undefined)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy|toBeNull|toBeUndefined)\s*\(\s*(?:true|1|null|undefined)?\s*\)'
107
+ r'|expect(?:\.soft|\.poll)?\s*\(\s*([a-zA-Z0-9_]+)\s*\)\s*\.\s*(?:toBe|toEqual)\s*\(\s*\1\s*\)'
102
108
  r'|\bassert\s+(?:True|true|1\s*==\s*1)\b'
103
- r'|\bassert\s+([a-zA-Z0-9_]+)\s*==\s*\1\b'
104
- r'|assertEquals\s*\(\s*(?:true|1)\s*,\s*(?:true|1)\s*\)'
109
+ r'|\bassert\s+([a-zA-Z0-9_\.]+(?:\([^)]*\))?)\s*==\s*\1\b'
110
+ r'|\bassert\s+(?:len\s*\([^)]*\)|[a-zA-Z0-9_\.]+\.(?:length|size\(\)))\s*(?:>=\s*0|>\s*-(?:1|0))\b'
111
+ r'|assertEquals\s*\(\s*(?:true|1|null)\s*,\s*(?:true|1|null)\s*\)'
105
112
  r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
106
113
  re.IGNORECASE
107
114
  )
108
115
 
116
+
117
+ def find_dangling_expects(clean_code, raw_lines):
118
+ """
119
+ Detects dangling expect(...) calls lacking chained matchers (e.g. expect(page) without .toBeVisible()).
120
+ Correctly handles nested parentheses like expect(page.locator('#id')).
121
+ """
122
+ dangling = []
123
+ pattern = re.compile(r'(?:await\s+)?\bexpect(?:\.soft|\.poll)?\s*\(')
124
+ for m in pattern.finditer(clean_code):
125
+ start_idx = m.end()
126
+ depth = 1
127
+ pos = start_idx
128
+ while pos < len(clean_code) and depth > 0:
129
+ ch = clean_code[pos]
130
+ if ch == '(':
131
+ depth += 1
132
+ elif ch == ')':
133
+ depth -= 1
134
+ elif ch in (';', '\n') and depth > 0:
135
+ # Malformed unclosed expect
136
+ break
137
+ pos += 1
138
+ if depth == 0:
139
+ rest = clean_code[pos:].lstrip()
140
+ if not rest.startswith('.'):
141
+ line_no = clean_code[:m.start()].count("\n") + 1
142
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
143
+ dangling.append((line_no, raw_line))
144
+ return dangling
145
+
109
146
  # Rule L4: Unawaited Playwright assertions in JS/TS
110
147
  # Expanded with all standard Playwright async matchers and optional .not modifier
111
148
  PLAYWRIGHT_ASYNC_MATCHERS = (
@@ -135,7 +172,7 @@ THREAD_SLEEP_PATTERN = MS_DELAY_PATTERN
135
172
 
136
173
  def strip_comments_and_strings(source_code, preserve_newlines=True):
137
174
  """
138
- Lexically strips block comments, line comments, multiline strings,
175
+ Lexically strips block comments, line comments, multiline strings,
139
176
  and quoted literals while preserving line numbering.
140
177
  Prevents false-positive / false-negative matches in AST linter.
141
178
  """
@@ -173,7 +210,7 @@ def _is_benign_empty_catch(clean_code, match_start):
173
210
  if not stripped:
174
211
  continue
175
212
  if re.match(r'try\s*(?:[:{]|$)', stripped):
176
- return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(l) for l in body_lines)
213
+ return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(bl) for bl in body_lines)
177
214
  body_lines.append(stripped)
178
215
  return False
179
216
 
@@ -185,7 +222,7 @@ def load_config(repo_root=None, config_path=None):
185
222
  with open(config_path, "r", encoding="utf-8") as f:
186
223
  return json.load(f)
187
224
  except (OSError, json.JSONDecodeError):
188
- config_data = {}
225
+ _ = False
189
226
 
190
227
  root = repo_root or REPO_ROOT
191
228
  candidates = [
@@ -300,7 +337,7 @@ def format_junit(report_data):
300
337
  return "\n".join(lines)
301
338
 
302
339
 
303
- def classify_file_role(file_path: str) -> str:
340
+ def classify_file_role(file_path: str, content: str = "") -> str:
304
341
  """
305
342
  Classifies a source file into:
306
343
  - 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
@@ -324,6 +361,8 @@ def classify_file_role(file_path: str) -> str:
324
361
  return "PAGE_OBJECT"
325
362
  if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
326
363
  return "PAGE_OBJECT"
364
+ if content and (re.search(r'\bclass\s+[A-Za-z0-9_]*Page\b', content) or re.search(r'\bextends\s+BasePage\b', content)):
365
+ return "PAGE_OBJECT"
327
366
 
328
367
  # 3. Helpers & Fixtures / Tools & Utilities / Benchmarks
329
368
  if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin", "benchmarks", "benchmark") for p in parts):
@@ -348,13 +387,16 @@ def classify_file_role(file_path: str) -> str:
348
387
 
349
388
  def analyze_script(file_path, baseline_set=None, disabled_rules=None):
350
389
  """Performs static rule scanning on a single test script or automation asset."""
351
- role = classify_file_role(file_path)
352
-
353
390
  try:
354
391
  rel_path = os.path.relpath(file_path, REPO_ROOT)
355
392
  except ValueError:
356
393
  rel_path = os.path.abspath(file_path)
357
394
 
395
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
396
+ content = f.read()
397
+
398
+ role = classify_file_role(file_path, content=content)
399
+
358
400
  # Config files have zero test assertions by definition
359
401
  if role == "CONFIG":
360
402
  return {
@@ -364,14 +406,11 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
364
406
  "is_clean": True
365
407
  }
366
408
 
367
- with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
368
- content = f.read()
369
-
370
409
  defects = []
371
410
 
372
411
  # Lexically strip comments and string literals for accurate analysis
373
412
  clean_code = strip_comments_and_strings(content, preserve_newlines=True)
374
- clean_lines = [l.strip() for l in clean_code.splitlines()]
413
+ clean_lines = [line.strip() for line in clean_code.splitlines()]
375
414
 
376
415
  raw_lines = content.splitlines()
377
416
 
@@ -402,12 +441,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
402
441
  elif arg.isdigit():
403
442
  delay_val = int(arg)
404
443
  if delay_val > 0:
405
- d_type = "EXCESSIVE_HARD_DELAY" if delay_val >= 3000 else "HARD_DELAY_ANTI_PATTERN"
444
+ is_excessive = delay_val >= 3000
445
+ d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
446
+ sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
406
447
  defects.append({
407
448
  "type": d_type,
408
- "severity": "WARNING",
449
+ "severity": sev,
409
450
  "line": idx,
410
- "message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
451
+ "message": f"Static sleep of {delay_val}ms found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
411
452
  })
412
453
  continue
413
454
  elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
@@ -428,12 +469,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
428
469
  if delay_val > 0:
429
470
  delay_ms = int(delay_val * 1000)
430
471
  delay_str = f"{delay_val:g}"
431
- d_type = "EXCESSIVE_HARD_DELAY" if delay_ms >= 3000 else "HARD_DELAY_ANTI_PATTERN"
472
+ is_excessive = delay_ms >= 3000
473
+ d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
474
+ sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
432
475
  defects.append({
433
476
  "type": d_type,
434
- "severity": "WARNING",
477
+ "severity": sev,
435
478
  "line": idx,
436
- "message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
479
+ "message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
437
480
  })
438
481
  except ValueError:
439
482
  if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
@@ -447,8 +490,8 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
447
490
  # Test Script Specific Invariants (Do NOT apply to Page Objects or Helpers)
448
491
  if role == "TEST_SCRIPT":
449
492
  executable_lines = [
450
- l for l in clean_lines
451
- if l and not l.startswith("import ") and not l.startswith("package ") and not l.startswith("from ")
493
+ line for line in clean_lines
494
+ if line and not line.startswith("import ") and not line.startswith("package ") and not line.startswith("from ")
452
495
  ]
453
496
  # Rule 1: Empty Test Script
454
497
  if len(executable_lines) == 0:
@@ -470,6 +513,68 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
470
513
  "message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
471
514
  })
472
515
 
516
+ # Rule 4.1: Dangling expect(...) calls without chained matcher
517
+ for d_line, d_raw in find_dangling_expects(clean_code, raw_lines):
518
+ defects.append({
519
+ "type": "DANGLING_ASSERTION_NO_MATCHER",
520
+ "severity": "CRITICAL",
521
+ "line": d_line,
522
+ "message": f"Dangling expect() call without matcher detected ('{d_raw}'). expect() returns an expectation object and does not perform any assertion without a matcher (e.g. .toBeVisible(), .toHaveText())."
523
+ })
524
+
525
+ # Rule 4.2: Python AST Compiler-Grade Verification for .py Test Scripts
526
+ if file_path.endswith(".py"):
527
+ try:
528
+ tree = ast.parse(content, filename=file_path)
529
+ for node in ast.walk(tree):
530
+ if isinstance(node, ast.Assert):
531
+ if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1:
532
+ op = node.test.ops[0]
533
+ right = node.test.comparators[0]
534
+ if isinstance(op, ast.Eq):
535
+ if ast.dump(node.test.left) == ast.dump(right):
536
+ line_no = getattr(node, "lineno", 1)
537
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
538
+ defects.append({
539
+ "type": "TAUTOLOGICAL_ASSERTION",
540
+ "severity": "CRITICAL",
541
+ "line": line_no,
542
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). Both sides of comparison evaluate to identical AST expressions."
543
+ })
544
+ elif isinstance(op, (ast.GtE, ast.Gt)):
545
+ if isinstance(node.test.left, ast.Call) and getattr(node.test.left.func, 'id', None) == 'len':
546
+ if isinstance(right, ast.Constant):
547
+ if (isinstance(op, ast.GtE) and right.value == 0) or (isinstance(op, ast.Gt) and right.value < 0):
548
+ line_no = getattr(node, "lineno", 1)
549
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
550
+ defects.append({
551
+ "type": "TAUTOLOGICAL_ASSERTION",
552
+ "severity": "CRITICAL",
553
+ "line": line_no,
554
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). len() is non-negative by definition."
555
+ })
556
+ elif isinstance(node.test, ast.Constant) and node.test.value is True:
557
+ line_no = getattr(node, "lineno", 1)
558
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
559
+ defects.append({
560
+ "type": "TAUTOLOGICAL_ASSERTION",
561
+ "severity": "CRITICAL",
562
+ "line": line_no,
563
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). Constant True assertion."
564
+ })
565
+ elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
566
+ if getattr(node.value.func, 'id', None) == 'expect':
567
+ line_no = getattr(node, "lineno", 1)
568
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
569
+ defects.append({
570
+ "type": "DANGLING_ASSERTION_NO_MATCHER",
571
+ "severity": "CRITICAL",
572
+ "line": line_no,
573
+ "message": f"Dangling expect() call without matcher detected via AST ('{raw_line}')."
574
+ })
575
+ except SyntaxError:
576
+ _ = False # synapse-ignore
577
+
473
578
  # Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
474
579
  if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
475
580
  for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
@@ -544,6 +649,15 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
544
649
  "message": "Test case file contains executable logic but has no verification or assertion statements."
545
650
  })
546
651
 
652
+ seen_keys = set()
653
+ deduped = []
654
+ for d in defects:
655
+ k = (d.get("type"), d.get("line"))
656
+ if k not in seen_keys:
657
+ seen_keys.add(k)
658
+ deduped.append(d)
659
+ defects = deduped
660
+
547
661
  active_defects = []
548
662
  disabled = {r.upper() for r in (disabled_rules or [])}
549
663
  norm_rel = rel_path.replace("\\", "/")
@@ -740,7 +854,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
740
854
  print(f" Target Scope : 0 Test Scripts discovered in {os.path.basename(scan_dir)}/ (No tests found)")
741
855
  print(f" Critical Defects : {GREEN}0{RESET}")
742
856
  print(f" Warnings / Suggestions : {GREEN}0{RESET}")
743
- print(f" Clean Scripts : 0/0 (N/A)")
857
+ print(" Clean Scripts : 0/0 (N/A)")
744
858
  print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
745
859
  else:
746
860
  print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
@@ -777,7 +891,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
777
891
  sys.stderr.write(f"Notice: Target report write failed: {err}\n")
778
892
 
779
893
  if strict:
780
- fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST"))
894
+ fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST", "DANGLING_ASSERTION_NO_MATCHER"))
781
895
  return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
782
896
  return 0 if critical_count == 0 else 1
783
897
 
@@ -16,7 +16,6 @@ Reference:
16
16
  - Synapse-Hub Subsystem 6: Section 6.7 of agent_skill_training_analysis.md.
17
17
  """
18
18
 
19
- import os
20
19
  import sys
21
20
  import re
22
21
  import json
@@ -163,16 +163,16 @@ def parse_csv_jtl(filepath: str) -> Tuple[Dict[str, SamplerStats], SamplerStats]
163
163
  # Column names can vary slightly across JMeter versions
164
164
  elapsed = float(row.get("elapsed") or row.get("Elapsed") or row.get("time") or 0)
165
165
  label = row.get("label") or row.get("Label") or "Unknown"
166
-
166
+
167
167
  success_val = row.get("success") or row.get("Success") or "true"
168
168
  success = str(success_val).strip().lower() in ("true", "1")
169
-
169
+
170
170
  ts_val = row.get("timeStamp") or row.get("Timestamp") or row.get("t") or 0
171
171
  timestamp = int(float(ts_val))
172
-
172
+
173
173
  bytes_val = row.get("bytes") or row.get("Bytes") or 0
174
174
  bytes_recv = int(float(bytes_val))
175
-
175
+
176
176
  resp_code = row.get("responseCode") or row.get("ResponseCode") or row.get("rc") or "200"
177
177
 
178
178
  if label not in per_sampler:
@@ -236,7 +236,7 @@ def evaluate_sla(
236
236
  ) -> Tuple[bool, List[str]]:
237
237
  """Evaluates computed aggregate metrics against SLA limits."""
238
238
  violations: List[str] = []
239
-
239
+
240
240
  if sla_p95 is not None and agg_stats["p95_ms"] > sla_p95:
241
241
  violations.append(f"P95 Latency SLA Breached: {agg_stats['p95_ms']}ms > {sla_p95}ms")
242
242
 
@@ -261,7 +261,7 @@ def format_table(per_sampler_data: List[Dict[str, Any]], agg_data: Dict[str, Any
261
261
  lines = []
262
262
  header_str = " | ".join(h.ljust(col_w[i]) for i, h in enumerate(headers))
263
263
  sep_str = "-+-".join("-" * col_w[i] for i in range(len(headers)))
264
-
264
+
265
265
  lines.append(header_str)
266
266
  lines.append(sep_str)
267
267
 
@@ -271,7 +271,7 @@ def format_table(per_sampler_data: List[Dict[str, Any]], agg_data: Dict[str, Any
271
271
  if len(label) > 28:
272
272
  label = label[:25] + "..."
273
273
  err_str = f"{row['errors']} ({row['error_rate_pct']}%)"
274
-
274
+
275
275
  row_str = " | ".join([
276
276
  label.ljust(col_w[0]),
277
277
  str(row["samples"]).rjust(col_w[1]),
@@ -301,7 +301,7 @@ def generate_markdown_report(
301
301
  ) -> str:
302
302
  """Generates GitHub / GitLab Markdown report for Pull Request annotations."""
303
303
  status_badge = "✅ **PASSED**" if passed else "❌ **FAILED (SLA BREACHED)**"
304
-
304
+
305
305
  lines = [
306
306
  "# 🚀 Apache JMeter Performance & Load Test Report",
307
307
  "",
@@ -377,7 +377,7 @@ def main():
377
377
  with open(temp_jtl, "w", encoding="utf-8", newline="") as f:
378
378
  writer = csv.writer(f)
379
379
  writer.writerow(["timeStamp", "elapsed", "label", "responseCode", "responseMessage", "threadName", "dataType", "success", "failureMessage", "bytes", "sentBytes", "grpThreads", "allThreads", "URL", "Latency", "IdleTime", "Connect"])
380
-
380
+
381
381
  # Generate 200 synthetic rows
382
382
  import time
383
383
  base_time = int(time.time() * 1000)
@@ -387,7 +387,7 @@ def main():
387
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
388
  for i in range(5):
389
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
-
390
+
391
391
  args.jtl_file = temp_jtl
392
392
  print(f"🔬 [SAMPLE MODE] Generated synthetic benchmark JTL: {temp_jtl}")
393
393
 
@@ -414,7 +414,7 @@ def main():
414
414
  print("=" * 90)
415
415
 
416
416
  # Print SLA Verdict
417
- print(f"\n🎯 [QUALITY GATE SLA VERDICT]")
417
+ print("\n🎯 [QUALITY GATE SLA VERDICT]")
418
418
  if passed:
419
419
  print("✅ ALL PERFORMANCE SLAs PASSED! Ready for deployment/merge.")
420
420
  else:
@@ -0,0 +1,30 @@
1
+ """
2
+ Synapse Hub — Static Quality & Policy Sentinels (tools.lint)
3
+ -----------------------------------------------------------
4
+ Domain package grouping all static code defect, encoding, license,
5
+ and AI documentation drift verification tools.
6
+ """
7
+
8
+ from tools import (
9
+ author_integrity,
10
+ check_version_sync,
11
+ git_pre_commit_guard,
12
+ infer_linter,
13
+ lint_ai_docs,
14
+ skill_collision_guard,
15
+ skillspector,
16
+ spec_drift_sentinel,
17
+ validate_groovy_scripts,
18
+ )
19
+
20
+ __all__ = [
21
+ "author_integrity",
22
+ "check_version_sync",
23
+ "git_pre_commit_guard",
24
+ "infer_linter",
25
+ "lint_ai_docs",
26
+ "skill_collision_guard",
27
+ "skillspector",
28
+ "spec_drift_sentinel",
29
+ "validate_groovy_scripts",
30
+ ]
@@ -281,8 +281,50 @@ def lint_docs(repo_root):
281
281
  if mname not in valid_methods:
282
282
  errors.append(f"[{doc}:{line_no}] Non-existent method referenced: '{cname}.{mname}()' (Class '{cname}' does not define this method)")
283
283
 
284
+ # 6. Documentation Metrics Consistency (Detects stale skill/tool/gate numbers)
285
+ metric_errors = check_doc_metrics_consistency(repo_root)
286
+ errors.extend(metric_errors)
287
+
284
288
  return errors, warnings
285
289
 
290
+
291
+ def check_doc_metrics_consistency(repo_root):
292
+ """Verifies that documented counts (skills, tools, DoD gates) accurately match disk."""
293
+ errors = []
294
+ skills_dir = os.path.join(repo_root, ".agents", "skills")
295
+ tools_dir = os.path.join(repo_root, "tools")
296
+
297
+ if not os.path.isdir(skills_dir) or not os.path.isdir(tools_dir):
298
+ return errors
299
+
300
+ skills_count = len([d for d in os.listdir(skills_dir) if os.path.isdir(os.path.join(skills_dir, d)) and os.path.exists(os.path.join(skills_dir, d, "SKILL.md"))])
301
+ tools_count = len([f for f in os.listdir(tools_dir) if f.endswith(".py") and f != "__init__.py"])
302
+
303
+ obsolete_patterns = [
304
+ (re.compile(r'\b(42|50)\s+(kỹ năng|skills?)\b', re.IGNORECASE), f"Obsolete skill count detected (real count: {skills_count})"),
305
+ (re.compile(r'\b(60|68)\s+(công cụ|tools?)\b', re.IGNORECASE), f"Obsolete tool count detected (real count: {tools_count})"),
306
+ (re.compile(r'\b9\s+Cổng\s+Chất\s+Lượng\b', re.IGNORECASE), "Obsolete gate count detected: must refer to 10 Cổng Chất Lượng DoD (Gate 0–9)"),
307
+ ]
308
+
309
+ docs_to_check = [
310
+ os.path.join(repo_root, "README.md"),
311
+ os.path.join(repo_root, "README.en.md"),
312
+ os.path.join(repo_root, "docs", "ARCHITECTURE.md"),
313
+ os.path.join(repo_root, "docs", "CONTRIBUTING.md"),
314
+ ]
315
+
316
+ for doc_path in docs_to_check:
317
+ if not os.path.exists(doc_path):
318
+ continue
319
+ rel_doc = os.path.relpath(doc_path, repo_root)
320
+ with open(doc_path, "r", encoding="utf-8", errors="ignore") as f:
321
+ for line_no, line in enumerate(f, 1):
322
+ for pattern, msg in obsolete_patterns:
323
+ if pattern.search(line):
324
+ errors.append(f"[{rel_doc}:{line_no}] {msg}: '{line.strip()}'")
325
+
326
+ return errors
327
+
286
328
  def main():
287
329
  import argparse
288
330
  parser = argparse.ArgumentParser(description="Enterprise AI Documentation & Artifact Linter")
@@ -87,7 +87,7 @@ def normalize_testcase_data(raw_data):
87
87
  return raw_data["testcases"]
88
88
  if "items" in raw_data and isinstance(raw_data["items"], list):
89
89
  return raw_data["items"]
90
-
90
+
91
91
  flat = []
92
92
  for k, v in raw_data.items():
93
93
  if isinstance(v, dict):
@@ -52,13 +52,15 @@ def estimate_test_duration(file_path: str, full_path: str) -> float:
52
52
  if os.path.exists(CACHE_DB_PATH):
53
53
  try:
54
54
  conn = sqlite3.connect(CACHE_DB_PATH)
55
- cur = conn.cursor()
56
- cur.execute("CREATE TABLE IF NOT EXISTS test_runtimes (test_path TEXT PRIMARY KEY, duration REAL)")
57
- cur.execute("SELECT duration FROM test_runtimes WHERE test_path = ?", (file_path,))
58
- row = cur.fetchone()
59
- conn.close()
60
- if row and row[0]:
61
- return float(row[0])
55
+ try:
56
+ cur = conn.cursor()
57
+ cur.execute("CREATE TABLE IF NOT EXISTS test_runtimes (test_path TEXT PRIMARY KEY, duration REAL)")
58
+ cur.execute("SELECT duration FROM test_runtimes WHERE test_path = ?", (file_path,))
59
+ row = cur.fetchone()
60
+ if row and row[0]:
61
+ return float(row[0])
62
+ finally:
63
+ conn.close()
62
64
  except sqlite3.Error:
63
65
  conn = None
64
66
 
@@ -93,14 +95,14 @@ def shard_tests_lpt(test_files: List[str], target_dir: str, num_shards: int) ->
93
95
  tests_with_cost.sort(key=lambda x: x[1], reverse=True)
94
96
 
95
97
  # 3. Initialize shards
96
- shards = [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
98
+ shards: List[Dict[str, Any]] = [{"shard_index": i + 1, "total_duration": 0.0, "tests": []} for i in range(num_shards)]
97
99
 
98
100
  # 4. Greedily assign to the shard with the least current cumulative duration
99
101
  for test_file, dur in tests_with_cost:
100
102
  # Find shard with min duration
101
- min_shard = min(shards, key=lambda s: s["total_duration"])
103
+ min_shard = min(shards, key=lambda s: float(s["total_duration"]))
102
104
  min_shard["tests"].append(test_file)
103
- min_shard["total_duration"] = round(min_shard["total_duration"] + dur, 2)
105
+ min_shard["total_duration"] = round(float(min_shard["total_duration"]) + dur, 2)
104
106
 
105
107
  return shards
106
108
 
@@ -119,9 +121,9 @@ def main():
119
121
  if args.json:
120
122
  print(json.dumps({"total_tests": len(test_files), "shards_count": args.shards, "shards": shards}, indent=2))
121
123
  else:
122
- print(f"\n======================================================================")
124
+ print("\n======================================================================")
123
125
  print(f" DURATION-BALANCED TEST MATRIX SHARDER ({args.shards} PARALLEL WORKERS) ")
124
- print(f"======================================================================")
126
+ print("======================================================================")
125
127
  print(f" Target Scope : {target_dir}")
126
128
  print(f" Total Tests : {len(test_files)} files\n")
127
129
 
@@ -137,7 +139,7 @@ def main():
137
139
  durations = [s["total_duration"] for s in shards]
138
140
  delta = max(durations) - min(durations) if durations else 0
139
141
  print(f"⚡ Shard Imbalance Delta: {delta:.1f}s (Optimal parallel distribution)")
140
- print(f"======================================================================\n")
142
+ print("======================================================================\n")
141
143
 
142
144
  return 0
143
145
 
@@ -0,0 +1,16 @@
1
+ """
2
+ Synapse Hub — Model Context Protocol (MCP) Servers (tools.mcp)
3
+ --------------------------------------------------------------
4
+ Domain package grouping all standard MCP services: core QA automation
5
+ MCP server and offline Excel spreadsheet manipulation server.
6
+ """
7
+
8
+ from tools import (
9
+ mcp_excel_server,
10
+ mcp_server,
11
+ )
12
+
13
+ __all__ = [
14
+ "mcp_excel_server",
15
+ "mcp_server",
16
+ ]
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
23
23
  except Exception:
24
24
  pass
25
25
 
26
- SERVER_VERSION = "2.7.0"
26
+ SERVER_VERSION = "3.0.1"
27
27
 
28
28
  NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
29
29
  NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
@@ -88,7 +88,7 @@ def read_sheet_rows(xlsx_path: str, sheet_name: Optional[str] = None, max_rows:
88
88
  for c in row.iter(f"{NS_MAIN}c"):
89
89
  cell_type = c.attrib.get("t")
90
90
  val_elem = c.find(f"{NS_MAIN}v")
91
- raw_val = val_elem.text if val_elem is not None else ""
91
+ raw_val = (val_elem.text or "") if val_elem is not None else ""
92
92
 
93
93
  if cell_type == "s" and raw_val.isdigit():
94
94
  idx = int(raw_val)