synapse-hub 2.4.2 → 2.5.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 (168) hide show
  1. package/.agents/agents/orchestrator.md +1 -0
  2. package/.agents/agents/pom-crafter.md +1 -0
  3. package/.agents/agents/quality-gatekeeper.md +6 -0
  4. package/.agents/rules/skill-precedence.md +38 -18
  5. package/.agents/rules/systematic-debugging.md +2 -0
  6. package/.agents/skills/lean-code-ladder/SKILL.md +12 -1
  7. package/.agents/skills/lean-impact-scoreboard/SKILL.md +48 -0
  8. package/.agents/skills/lean-impact-scoreboard/examples/sample_benchmark_gain.md +20 -0
  9. package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +15 -0
  10. package/.agents/skills/repo-quality-audit/SKILL.md +68 -0
  11. package/.agents/skills/repo-quality-audit/examples/sample_audit_walkthrough.md +15 -0
  12. package/.agents/skills/repo-quality-audit/examples/sample_repo_audit.md +33 -0
  13. package/.agents/skills/senior-code-review/SKILL.md +90 -0
  14. package/.agents/skills/senior-code-review/examples/sample_review_walkthrough.md +29 -0
  15. package/.agents/skills/technical-debt-harvester/SKILL.md +70 -0
  16. package/.agents/skills/technical-debt-harvester/examples/sample_debt_ledger.md +25 -0
  17. package/.agents/skills/technical-debt-harvester/examples/sample_ledger_output.md +20 -0
  18. package/.cursor/rules/synapse-qa.mdc +4 -3
  19. package/.cursorrules +1 -0
  20. package/.windsurfrules +1 -0
  21. package/AGENTS.md +51 -179
  22. package/CLAUDE.md +5 -0
  23. package/README.en.md +12 -8
  24. package/README.md +12 -8
  25. package/bin/agent-qa.js +3 -2
  26. package/docs/ARCHITECTURE.md +3 -3
  27. package/docs/CLI_REFERENCE.md +15 -1
  28. package/package.json +1 -3
  29. package/skills_manifest.json +38 -2
  30. package/synapse.config.json +1 -1
  31. package/templates/ai_bridges/CLAUDE.md +5 -0
  32. package/templates/ai_bridges/copilot-instructions.md +2 -0
  33. package/templates/ai_bridges/cursor_synapse_qa.mdc +4 -3
  34. package/templates/ai_bridges/windsurfrules +1 -0
  35. package/tools/.infer_baseline.json +7 -1079
  36. package/tools/TOOLS_RUNBOOK.md +53 -13
  37. package/tools/__init__.py +1 -1
  38. package/tools/agent_canary.py +4 -4
  39. package/tools/analyze_project.py +2 -2
  40. package/tools/api_fuzzer.py +4 -4
  41. package/tools/auto_heal.py +4 -2
  42. package/tools/browser_helpers.js +2 -2
  43. package/tools/bump_version.py +30 -2
  44. package/tools/cache_resolver.py +6 -6
  45. package/tools/check_env.py +82 -18
  46. package/tools/cli.py +265 -197
  47. package/tools/count_tokens.py +7 -7
  48. package/tools/dashboard_generator.py +14 -14
  49. package/tools/db_sandbox.py +5 -5
  50. package/tools/debt_harvester.py +236 -0
  51. package/tools/distill_log.py +2 -2
  52. package/tools/flaky_detector.py +5 -5
  53. package/tools/generate_tc_from_story.py +2 -2
  54. package/tools/generate_test_reports.py +4 -4
  55. package/tools/git_pre_commit_guard.py +6 -6
  56. package/tools/infer_linter.py +12 -7
  57. package/tools/lint_ai_docs.py +2 -2
  58. package/tools/matrix_sharder.py +4 -4
  59. package/tools/mcp_excel_server.py +1 -1
  60. package/tools/mcp_server.py +3 -3
  61. package/tools/micro_mutator.py +6 -6
  62. package/tools/omni_transpiler.py +2 -2
  63. package/tools/output_formatter.py +37 -2
  64. package/tools/pinpoint_failure.py +4 -1
  65. package/tools/repo_map.py +2 -2
  66. package/tools/safety_firewall.py +2 -2
  67. package/tools/senior_code_review.py +341 -0
  68. package/tools/setup_project.py +17 -9
  69. package/tools/skillspector.py +2 -2
  70. package/tools/sync_agent_rules.py +14 -3
  71. package/tools/test_impact.py +4 -4
  72. package/tools/tms_sync_bridge.py +2 -2
  73. package/tools/update_sentinel.py +24 -23
  74. package/tools/verify_all.py +12 -12
  75. package/tools/visual_som_helper.py +8 -8
  76. package/tools/watch_mode.py +6 -7
  77. package/tools/wcag_audit.py +5 -1
  78. package/tools/welcome_page.py +11 -11
  79. package/benchmarks/qa_eval/README.md +0 -71
  80. package/benchmarks/qa_eval/build_dataset.py +0 -677
  81. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +0 -22
  82. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +0 -22
  83. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +0 -22
  84. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +0 -22
  85. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +0 -22
  86. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +0 -21
  87. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +0 -21
  88. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +0 -21
  89. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +0 -21
  90. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +0 -21
  91. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +0 -23
  92. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +0 -23
  93. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +0 -23
  94. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +0 -23
  95. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +0 -23
  96. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +0 -22
  97. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +0 -22
  98. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +0 -22
  99. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +0 -22
  100. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +0 -23
  101. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +0 -22
  102. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +0 -22
  103. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +0 -22
  104. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +0 -22
  105. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +0 -22
  106. package/benchmarks/qa_eval/eval_runner.py +0 -418
  107. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +0 -94
  108. package/benchmarks/qa_eval/reports/eval_results.json +0 -461
  109. package/examples/cypress-ecommerce-suite/README.md +0 -45
  110. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +0 -17
  111. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +0 -29
  112. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +0 -51
  113. package/examples/cypress-ecommerce-suite/cypress.config.js +0 -13
  114. package/examples/cypress-ecommerce-suite/package.json +0 -13
  115. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +0 -19
  116. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +0 -29
  117. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +0 -45
  118. package/examples/katalon-suite/Profiles/default.glbl +0 -12
  119. package/examples/katalon-suite/README.md +0 -18
  120. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +0 -12
  121. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +0 -15
  122. package/examples/katalon-suite/katalon_suite.prj +0 -21
  123. package/examples/playwright-crm-suite/README.md +0 -40
  124. package/examples/playwright-crm-suite/package.json +0 -14
  125. package/examples/playwright-crm-suite/pages/BasePage.ts +0 -37
  126. package/examples/playwright-crm-suite/pages/CustomerPage.ts +0 -57
  127. package/examples/playwright-crm-suite/pages/LoginPage.ts +0 -32
  128. package/examples/playwright-crm-suite/playwright.config.ts +0 -27
  129. package/examples/playwright-crm-suite/tests/auth.spec.ts +0 -22
  130. package/examples/playwright-crm-suite/tests/customers.spec.ts +0 -33
  131. package/examples/plugins/sample-custom-plugin/README.md +0 -14
  132. package/examples/plugins/sample-custom-plugin/plugin.json +0 -16
  133. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +0 -29
  134. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +0 -8
  135. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +0 -125
  136. package/examples/practices/README.md +0 -22
  137. package/examples/practices/requirements/crm/requirements_contracts.md +0 -292
  138. package/examples/practices/requirements/crm/requirements_customers.md +0 -462
  139. package/examples/practices/requirements/crm/requirements_login.md +0 -202
  140. package/examples/practices/requirements/crm/requirements_projects.md +0 -396
  141. package/examples/practices/requirements/crm/requirements_tasks.md +0 -343
  142. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +0 -402
  143. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  144. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +0 -52
  145. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  146. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  147. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +0 -119
  148. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +0 -9
  149. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  150. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  151. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +0 -32
  152. package/examples/practices/testcases/api/book_management_api_test_cases.md +0 -144
  153. package/examples/practices/testcases/api/user_management_api_test_cases.md +0 -139
  154. package/examples/practices/testcases/crm/test_cases_crm_login.md +0 -148
  155. package/examples/practices/testcases/crm/test_cases_customers.md +0 -156
  156. package/examples/pytest-api-suite/README.md +0 -45
  157. package/examples/pytest-api-suite/conftest.py +0 -31
  158. package/examples/pytest-api-suite/pages/user_api_client.py +0 -36
  159. package/examples/pytest-api-suite/requirements.txt +0 -2
  160. package/examples/pytest-api-suite/tests/test_user_api.py +0 -44
  161. package/examples/selenium-suite/README.md +0 -18
  162. package/examples/selenium-suite/pages/base_page.py +0 -27
  163. package/examples/selenium-suite/pages/login_page.py +0 -29
  164. package/examples/selenium-suite/pages/order_page.py +0 -25
  165. package/examples/selenium-suite/pytest.ini +0 -6
  166. package/examples/selenium-suite/requirements.txt +0 -2
  167. package/examples/selenium-suite/tests/test_login.py +0 -10
  168. package/examples/selenium-suite/tests/test_order.py +0 -10
@@ -140,7 +140,7 @@ def get_budget_config() -> Dict[str, int]:
140
140
  "budget_7d": data.get("budget_7d", DEFAULT_7D_BUDGET),
141
141
  }
142
142
  except (OSError, json.JSONDecodeError):
143
- pass
143
+ custom_budgets = None
144
144
  return {
145
145
  "budget_5h": DEFAULT_5H_BUDGET,
146
146
  "budget_7d": DEFAULT_7D_BUDGET,
@@ -208,7 +208,7 @@ class AntigravityAdapter:
208
208
  }
209
209
  conn.close()
210
210
  except (sqlite3.Error, OSError):
211
- pass
211
+ conn_closed = True
212
212
 
213
213
  conversations = []
214
214
  for entry in os.listdir(brain_dir):
@@ -388,7 +388,7 @@ class AntigravityAdapter:
388
388
  usage_hours["cache"] += cache_tok
389
389
  usage_hours["output"] += out_tok
390
390
  except (OSError, UnicodeDecodeError):
391
- pass
391
+ transcript_read = False
392
392
 
393
393
  budgets = get_budget_config()
394
394
  b_5h = budgets["budget_5h"]
@@ -665,7 +665,7 @@ def detect_platform_from_file(file_path: str) -> str:
665
665
  if "claude" in line.lower():
666
666
  return "claude"
667
667
  except (OSError, UnicodeDecodeError):
668
- pass
668
+ model_detected = None
669
669
  return "antigravity"
670
670
 
671
671
 
@@ -985,7 +985,7 @@ def export_opentelemetry_genai_spans(parsed: Dict[str, Any], session_id: str = "
985
985
  - gen_ai.usage.cache_read_tokens
986
986
  - tool.name / tool.system
987
987
  """
988
- trace_id = hashlib.md5(session_id.encode('utf-8')).hexdigest()
988
+ trace_id = hashlib.md5(session_id.encode('utf-8'), usedforsecurity=False).hexdigest()
989
989
  platform = parsed.get("platform", "antigravity")
990
990
  model = parsed.get("detected_model", "gemini-flash")
991
991
 
@@ -1000,7 +1000,7 @@ def export_opentelemetry_genai_spans(parsed: Dict[str, Any], session_id: str = "
1000
1000
  spans = []
1001
1001
  turns = parsed.get("turns", [])
1002
1002
  for idx, turn in enumerate(turns, 1):
1003
- turn_span_id = hashlib.md5(f"{session_id}_turn_{idx}".encode('utf-8')).hexdigest()[:16]
1003
+ turn_span_id = hashlib.md5(f"{session_id}_turn_{idx}".encode('utf-8'), usedforsecurity=False).hexdigest()[:16]
1004
1004
  turn_start_nano = int(time.time() * 1e9) - (len(turns) - idx) * int(1e9)
1005
1005
 
1006
1006
  turn_span = {
@@ -1025,7 +1025,7 @@ def export_opentelemetry_genai_spans(parsed: Dict[str, Any], session_id: str = "
1025
1025
  spans.append(turn_span)
1026
1026
 
1027
1027
  for t_idx, tool_name in enumerate(turn.get("tools_called", []), 1):
1028
- tool_span_id = hashlib.md5(f"{session_id}_turn_{idx}_tool_{t_idx}".encode('utf-8')).hexdigest()[:16]
1028
+ tool_span_id = hashlib.md5(f"{session_id}_turn_{idx}_tool_{t_idx}".encode('utf-8'), usedforsecurity=False).hexdigest()[:16]
1029
1029
  tool_span = {
1030
1030
  "traceId": trace_id,
1031
1031
  "spanId": tool_span_id,
@@ -14,7 +14,7 @@ Supports Dual-Mode:
14
14
  1. TARGET PROJECT MODE: Displays target project test suites, POM PageClasses,
15
15
  automation tech stack (Playwright/Katalon/Pytest/Cypress), in-project 9 Gates DoD,
16
16
  and active AI skill profile.
17
- 2. HUB ECOSYSTEM MODE: Displays central Synapse-Hub catalog (38 skills),
17
+ 2. HUB ECOSYSTEM MODE: Displays central Synapse-Hub catalog (42 skills),
18
18
  self-healing benchmark (25 challenges), and platform governance.
19
19
 
20
20
  Features:
@@ -183,8 +183,8 @@ def load_manifest(hub_root: str) -> Dict[str, Any]:
183
183
  with open(path, "r", encoding="utf-8") as f:
184
184
  return json.load(f)
185
185
  except (OSError, json.JSONDecodeError):
186
- pass
187
- return {"totalSkills": 38, "skills": [], "categories": {}, "profiles": {}}
186
+ manifest_data = None
187
+ return {"totalSkills": 42, "skills": [], "categories": {}, "profiles": {}}
188
188
 
189
189
 
190
190
  def load_benchmark_report(hub_root: str) -> Dict[str, Any]:
@@ -209,7 +209,7 @@ def load_benchmark_report(hub_root: str) -> Dict[str, Any]:
209
209
  "available": True
210
210
  }
211
211
  except (OSError, UnicodeDecodeError):
212
- pass
212
+ report_found = False
213
213
  return {
214
214
  "resolution_rate": "100.0%",
215
215
  "mttr": "0.0s",
@@ -242,7 +242,7 @@ def load_health_report(target_dir: str) -> Dict[str, Any]:
242
242
  "available": True
243
243
  }
244
244
  except (OSError, UnicodeDecodeError):
245
- pass
245
+ report_found = False
246
246
  return {
247
247
  "dod_status": "100%_PASS",
248
248
  "classes_count": 0,
@@ -308,7 +308,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
308
308
  "report_path": rel_path
309
309
  }
310
310
  except (ET.ParseError, OSError, ValueError, KeyError):
311
- pass
311
+ parsed_xml = False
312
312
 
313
313
  # Check summaryTemp.txt if JUnit report not parsed
314
314
  summary_files = list(target_path.glob("**/summaryTemp.txt"))
@@ -341,7 +341,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
341
341
  "report_path": str(sf.relative_to(target_path))
342
342
  }
343
343
  except (OSError, ValueError):
344
- pass
344
+ parsed_summary = False
345
345
 
346
346
  return {
347
347
  "available": False,
@@ -401,7 +401,7 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
401
401
  if classes_list:
402
402
  break
403
403
  except (json.JSONDecodeError, OSError, KeyError):
404
- pass
404
+ json_parsed = False
405
405
 
406
406
  # Priority B: Parse repo_map.txt
407
407
  if not classes_list:
@@ -451,7 +451,7 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
451
451
  if classes_list:
452
452
  break
453
453
  except (OSError, UnicodeDecodeError):
454
- pass
454
+ continue
455
455
 
456
456
  # Priority C: Scan filesystem directly if repo_map files were missing
457
457
  if not classes_list:
@@ -488,7 +488,7 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
488
488
  "methods": m_list or ["(Defined in source)"]
489
489
  })
490
490
  except (OSError, UnicodeDecodeError):
491
- pass
491
+ report_found = False
492
492
 
493
493
  # 2. Discover Test Cases
494
494
  test_files = []
@@ -1766,7 +1766,7 @@ def generate_hub_html(
1766
1766
  now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1767
1767
  total_skills = manifest.get("totalSkills", len(manifest.get("skills", [])))
1768
1768
  if total_skills == 0:
1769
- total_skills = 38
1769
+ total_skills = 42
1770
1770
  skills = manifest.get("skills", [])
1771
1771
 
1772
1772
  gates = [
@@ -1885,7 +1885,7 @@ def generate_hub_html(
1885
1885
 
1886
1886
  translations_vi = {
1887
1887
  "header_title": "Bảng Điều Hành Chất Lượng Doanh Nghiệp",
1888
- "header_subtitle": "Synapse-Hub v2.4.2 • Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1888
+ "header_subtitle": "Synapse-Hub v2.5.0 • Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1889
1889
  "nav_welcome": "Giới Thiệu",
1890
1890
  "meta_mode": "🏷️ Chế độ: <strong>HỆ SINH THÁI QA HUB</strong>",
1891
1891
  "meta_gen": f"🕒 Khởi tạo: <strong>{now_str}</strong>",
@@ -1963,7 +1963,7 @@ def generate_hub_html(
1963
1963
 
1964
1964
  translations_en = {
1965
1965
  "header_title": "Enterprise QA Executive Dashboard",
1966
- "header_subtitle": "Synapse-Hub v2.4.2 &bull; Autonomous AI Agent Testing Ecosystem",
1966
+ "header_subtitle": "Synapse-Hub v2.5.0 &bull; Autonomous AI Agent Testing Ecosystem",
1967
1967
  "nav_welcome": "Welcome",
1968
1968
  "meta_mode": "🏷️ Mode: <strong>QA HUB ECOSYSTEM</strong>",
1969
1969
  "meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
@@ -2429,7 +2429,7 @@ def generate_hub_html(
2429
2429
  </div>
2430
2430
  {timeline_section_html}
2431
2431
 
2432
- <!-- 38 Specialized Skills Catalog -->
2432
+ <!-- 42 Specialized Skills Catalog -->
2433
2433
  <div class="section-header">
2434
2434
  <h2 data-i18n="sec_skills_title">{t['sec_skills_title']}</h2>
2435
2435
  <span class="section-badge" data-i18n="sec_skills_badge">{t['sec_skills_badge']}</span>
@@ -143,8 +143,8 @@ class DBSandbox:
143
143
  with open(meta_file, "r", encoding="utf-8") as f:
144
144
  meta = json.load(f)
145
145
  self.db_path = meta.get("source_db", "")
146
- except Exception:
147
- pass
146
+ except (OSError, json.JSONDecodeError):
147
+ meta = {}
148
148
  if not self.db_path:
149
149
  self.db_path = os.path.join(self.snapshot_dir, "active_test.db")
150
150
 
@@ -212,8 +212,8 @@ class DBSandbox:
212
212
  try:
213
213
  cursor.execute(f"ROLLBACK TO SAVEPOINT {savepoint_name};")
214
214
  conn.close()
215
- except Exception:
216
- pass
215
+ except sqlite3.Error as rollback_err:
216
+ sys.stderr.write(f"Notice: Savepoint rollback notice: {rollback_err}\n")
217
217
  return {
218
218
  "status": "ERROR",
219
219
  "error": str(e),
@@ -340,7 +340,7 @@ def _probe_http(target: str) -> Tuple[bool, str]:
340
340
  such as 401/403 still proves the service is up)."""
341
341
  try:
342
342
  req = urllib.request.Request(target, method="GET")
343
- with urllib.request.urlopen(req, timeout=2.0) as resp:
343
+ with urllib.request.urlopen(req, timeout=2.0) as resp: # nosec B310
344
344
  return True, f"http {resp.getcode()}"
345
345
  except urllib.error.HTTPError as exc:
346
346
  if exc.code < 500:
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
3
+ # SPDX-License-Identifier: MIT
4
+ # Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
5
+
6
+ """
7
+ tools/debt_harvester.py — Technical Debt & Shortcut Harvester
8
+ -----------------------------------------------------------
9
+ Scans repositories for deliberate simplification markers ('shortcut:' or 'ponytail:').
10
+ Compiles them into a structured Technical Debt Ledger so pragmatic compromises
11
+ and deferred upgrades are tracked and do not rot into forgotten technical debt.
12
+
13
+ Usage:
14
+ python3 tools/debt_harvester.py [target_dir]
15
+ python3 tools/debt_harvester.py [target_dir] --output DEBT_LEDGER.md
16
+ python3 tools/debt_harvester.py [target_dir] --json
17
+ python3 tools/debt_harvester.py [target_dir] --keyword "custom-tag:"
18
+ """
19
+
20
+ import os
21
+ import sys
22
+ import re
23
+ import json
24
+ import argparse
25
+ from typing import Dict, List, Any, Optional
26
+
27
+ # UTF-8 stdout encoding enforcement
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
+ # Default directories to ignore during scan
36
+ IGNORED_DIRS = {
37
+ ".git", "node_modules", "dist", "build", ".next", "out",
38
+ "venv", ".venv", "env", ".env", "__pycache__", ".idea",
39
+ ".vscode", "coverage", ".mypy_cache", ".ruff_cache",
40
+ ".pytest_cache", ".sandbox_snapshots", "graphify-out",
41
+ "claude-ecosystem-repos"
42
+ }
43
+
44
+ # Supported comment markers across major languages
45
+ # Supports: # (Python, Bash, YAML), // (JS, TS, Java, Groovy, C, C++),
46
+ # /* ... */, -- (SQL, Lua), <!-- (HTML, XML, Markdown)
47
+ DEBT_REGEX = re.compile(
48
+ r'(?:#|//|/\*|--|<!--)\s*(?:shortcut|ponytail):\s*(.+?)(?:\*/|-->)?$',
49
+ re.IGNORECASE
50
+ )
51
+
52
+
53
+ class ShortcutEntry:
54
+ def __init__(self, file_path: str, line_no: int, raw_comment: str):
55
+ self.file_path = file_path
56
+ self.line_no = line_no
57
+ self.raw_comment = raw_comment.strip()
58
+ self.description = ""
59
+ self.ceiling = ""
60
+ self.upgrade_trigger = ""
61
+ self.has_trigger = False
62
+ self._parse()
63
+
64
+ def _parse(self):
65
+ text = self.raw_comment
66
+ # Format patterns:
67
+ # 1. shortcut: <desc>, ceiling: <limit>, upgrade: <trigger>
68
+ # 2. shortcut: <desc>, <limit>, <trigger>
69
+ # 3. shortcut: <limit>, <when to upgrade>
70
+
71
+ parts = [p.strip() for p in text.split(',') if p.strip()]
72
+
73
+ # Check explicit labels
74
+ ceiling_match = re.search(r'(?:ceiling|limit):\s*([^,]+)', text, re.IGNORECASE)
75
+ upgrade_match = re.search(r'(?:upgrade|when|trigger):\s*([^,]+)', text, re.IGNORECASE)
76
+
77
+ if ceiling_match or upgrade_match:
78
+ self.ceiling = ceiling_match.group(1).strip() if ceiling_match else ""
79
+ self.upgrade_trigger = upgrade_match.group(1).strip() if upgrade_match else ""
80
+ # Description is what's left before ceiling/upgrade
81
+ desc_part = re.split(r'(?:ceiling|limit|upgrade|when|trigger):', text, flags=re.IGNORECASE)[0]
82
+ self.description = desc_part.strip().rstrip(',')
83
+ elif len(parts) >= 2:
84
+ self.description = parts[0]
85
+ self.ceiling = parts[1]
86
+ if len(parts) >= 3:
87
+ self.upgrade_trigger = parts[2]
88
+ else:
89
+ self.upgrade_trigger = parts[1]
90
+ elif len(parts) == 1:
91
+ self.description = parts[0]
92
+ self.ceiling = "Not specified"
93
+ self.upgrade_trigger = ""
94
+
95
+ if self.upgrade_trigger:
96
+ self.has_trigger = True
97
+
98
+ def to_dict(self) -> Dict[str, Any]:
99
+ return {
100
+ "file": self.file_path,
101
+ "line": self.line_no,
102
+ "description": self.description or self.raw_comment,
103
+ "ceiling": self.ceiling or "Not specified",
104
+ "upgrade_trigger": self.upgrade_trigger or "No trigger specified",
105
+ "has_trigger": self.has_trigger,
106
+ "raw": self.raw_comment
107
+ }
108
+
109
+
110
+ def scan_directory(target_dir: str, keyword: Optional[str] = None) -> List[ShortcutEntry]:
111
+ """Scans all non-ignored source files in target_dir for shortcut/debt markers."""
112
+ target_dir = os.path.abspath(target_dir)
113
+ results: List[ShortcutEntry] = []
114
+
115
+ pattern = DEBT_REGEX
116
+ if keyword:
117
+ escaped = re.escape(keyword.rstrip(':'))
118
+ pattern = re.compile(
119
+ rf'(?:#|//|/\*|--|<!--)\s*{escaped}:\s*(.+?)(?:\*/|-->)?$',
120
+ re.IGNORECASE
121
+ )
122
+
123
+ for root, dirs, files in os.walk(target_dir):
124
+ # Exclude ignored directories in-place
125
+ dirs[:] = [d for d in dirs if d not in IGNORED_DIRS and not d.startswith('.')]
126
+
127
+ for f in sorted(files):
128
+ # Skip non-text or binary files
129
+ ext = os.path.splitext(f)[1].lower()
130
+ if ext in {".png", ".jpg", ".jpeg", ".svg", ".zip", ".tar", ".gz", ".lock", ".ico", ".pdf"}:
131
+ continue
132
+
133
+ file_path = os.path.join(root, f)
134
+ rel_path = os.path.relpath(file_path, target_dir)
135
+
136
+ try:
137
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
138
+ for line_idx, line in enumerate(fh, start=1):
139
+ match = pattern.search(line)
140
+ if match:
141
+ comment_body = match.group(1).strip()
142
+ # Filter false positives like documentation describing the marker itself
143
+ if "shortcut: <" in comment_body or "shortcut: (" in comment_body:
144
+ continue
145
+ results.append(ShortcutEntry(rel_path, line_idx, comment_body))
146
+ except Exception:
147
+ continue
148
+
149
+ return results
150
+
151
+
152
+ def format_markdown(entries: List[ShortcutEntry], target_dir: str) -> str:
153
+ """Renders shortcut debt entries into a structured Markdown ledger."""
154
+ total = len(entries)
155
+ no_trigger = sum(1 for e in entries if not e.has_trigger)
156
+
157
+ lines = [
158
+ "# Technical Debt & Shortcut Ledger",
159
+ "",
160
+ "> **Ledger Policy:** Deliberate shortcuts prevent over-engineering. Track limits and upgrade triggers so they never rot silently into permanent defects.",
161
+ "",
162
+ f"- **Repository:** `{os.path.basename(target_dir)}`",
163
+ f"- **Total Shortcuts:** `{total}`",
164
+ f"- **Missing Upgrade Triggers:** `{no_trigger}` " + ("⚠️ *(Action Required)*" if no_trigger > 0 else "✅ *(All tracked)*"),
165
+ "",
166
+ "---",
167
+ "",
168
+ ]
169
+
170
+ if not entries:
171
+ lines.append("🎉 **No shortcut debt found. Clean ledger.**")
172
+ return "\n".join(lines)
173
+
174
+ lines.append("| Location | Description | Ceiling (Limit) | Upgrade Trigger | Status |")
175
+ lines.append("| :--- | :--- | :--- | :--- | :--- |")
176
+
177
+ for e in entries:
178
+ status = "✅ Tracked" if e.has_trigger else "⚠️ `no-trigger`"
179
+ loc = f"`{e.file_path}:{e.line_no}`"
180
+ lines.append(f"| {loc} | {e.description} | {e.ceiling} | {e.upgrade_trigger or 'None'} | {status} |")
181
+
182
+ lines.append("")
183
+ lines.append("### Recommended Action")
184
+ if no_trigger > 0:
185
+ lines.append(f"1. Review the `{no_trigger}` shortcut(s) tagged `no-trigger` above and define concrete upgrade conditions.")
186
+ else:
187
+ lines.append("1. All tracked shortcuts have defined upgrade boundaries. Review them during sprint refactoring cycles.")
188
+ lines.append("2. Run `agent-qa debt` in CI to monitor debt accumulation.")
189
+
190
+ return "\n".join(lines)
191
+
192
+
193
+ def run_debt_harvest(target_dir: str, keyword: Optional[str] = None, output_file: Optional[str] = None, as_json: bool = False) -> int:
194
+ """Executes the debt scan and prints/writes results."""
195
+ entries = scan_directory(target_dir, keyword)
196
+ total = len(entries)
197
+ no_trigger = sum(1 for e in entries if not e.has_trigger)
198
+
199
+ if as_json:
200
+ payload = {
201
+ "target": os.path.abspath(target_dir),
202
+ "total": total,
203
+ "no_trigger_count": no_trigger,
204
+ "items": [e.to_dict() for e in entries]
205
+ }
206
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
207
+ return 0
208
+
209
+ md_report = format_markdown(entries, target_dir)
210
+
211
+ if output_file:
212
+ out_path = os.path.abspath(output_file)
213
+ with open(out_path, "w", encoding="utf-8") as f:
214
+ f.write(md_report)
215
+ print(f"\n✅ Technical Debt Ledger saved to: {out_path} ({total} entries)")
216
+ else:
217
+ print("\n" + md_report)
218
+
219
+ return 0
220
+
221
+
222
+ def main():
223
+ parser = argparse.ArgumentParser(
224
+ description="Harvest shortcut: and deliberate debt comments into a tracked technical debt ledger."
225
+ )
226
+ parser.add_argument("target", nargs="?", default=".", help="Directory to scan (default: current directory)")
227
+ parser.add_argument("--keyword", "-k", default=None, help="Custom marker keyword (default: shortcut or ponytail)")
228
+ parser.add_argument("--output", "-o", default=None, help="Save report to Markdown file")
229
+ parser.add_argument("--json", action="store_true", help="Output results in JSON format")
230
+
231
+ args = parser.parse_args()
232
+ sys.exit(run_debt_harvest(args.target, args.keyword, args.output, args.json))
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()
@@ -149,8 +149,8 @@ def main():
149
149
  os.makedirs(os.path.dirname(os.path.abspath(args.save_raw)), exist_ok=True)
150
150
  with open(args.save_raw, "w", encoding="utf-8") as fp:
151
151
  fp.write(log_text)
152
- except Exception:
153
- pass
152
+ except OSError as err:
153
+ sys.stderr.write(f"Warning: Could not save raw log: {err}\n")
154
154
 
155
155
  summary = distill(log_text)
156
156
  print(summary)
@@ -15,10 +15,10 @@ Features:
15
15
  """
16
16
 
17
17
  import os
18
- import re
19
18
  import sys
20
19
  import glob
21
20
  import json
21
+ import sqlite3
22
22
  import time
23
23
  import shlex
24
24
  import argparse
@@ -187,8 +187,8 @@ def benchmark_live_command(cmd: str, iterations: int = 5, test_id: Optional[str]
187
187
  error_signature="NON_ZERO_EXIT" if not is_pass else "",
188
188
  context=cmd
189
189
  )
190
- except Exception:
191
- pass
190
+ except sqlite3.Error as db_err:
191
+ sys.stderr.write(f"Notice: Run recording skipped: {db_err}\n")
192
192
 
193
193
  status_str = f"{GREEN}PASS{RESET}" if is_pass else f"{RED}FAIL (Exit {code}){RESET}"
194
194
  print(f"{status_str} ({ms} ms)")
@@ -362,8 +362,8 @@ def main():
362
362
  duration_ms=0.0,
363
363
  context=args.reports
364
364
  )
365
- except Exception:
366
- pass
365
+ except sqlite3.Error as db_err:
366
+ sys.stderr.write(f"Notice: Run recording skipped: {db_err}\n")
367
367
 
368
368
  if st["quarantine"]:
369
369
  quarantine_candidates.append(st)
@@ -43,8 +43,8 @@ def detect_project_package(repo_root="."):
43
43
  m = re.search(r'import\s+([\w\.]+)\.pages\.', content)
44
44
  if m:
45
45
  return f"{m.group(1)}.pages"
46
- except Exception:
47
- pass
46
+ except (OSError, UnicodeDecodeError):
47
+ content = ""
48
48
 
49
49
  # 2. Check existing Keywords/ directory
50
50
  keywords_dir = os.path.join(repo_root, "Keywords")
@@ -27,8 +27,8 @@ def find_latest_junit_report(search_dirs):
27
27
  try:
28
28
  mtime = os.path.getmtime(p)
29
29
  candidates.append((mtime, p))
30
- except OSError:
31
- pass
30
+ except OSError as err:
31
+ sys.stderr.write(f"Notice: getmtime skipped: {err}\n")
32
32
  if not candidates:
33
33
  return None
34
34
  candidates.sort(key=lambda x: x[0], reverse=True)
@@ -96,8 +96,8 @@ def generate_reports(target_dir=None, project_name="Enterprise-Automation"):
96
96
  if "Passed" in content:
97
97
  print(f"[+] Found existing summary file: {existing}")
98
98
  return 0
99
- except OSError:
100
- pass
99
+ except OSError as err:
100
+ sys.stderr.write(f"Notice: Read summaryTemp skipped: {err}\n")
101
101
 
102
102
  latest_xml = find_latest_junit_report([reports_dir, test_reports_dir, root_dir])
103
103
  if latest_xml:
@@ -41,8 +41,8 @@ def check_bom(files):
41
41
  with open(f, "rb") as fp:
42
42
  if fp.read(3) == b"\xef\xbb\xbf":
43
43
  bom_files.append(f)
44
- except Exception:
45
- pass
44
+ except OSError as err:
45
+ sys.stderr.write(f"Notice: Could not read {f}: {err}\n")
46
46
  return bom_files
47
47
 
48
48
  def check_forbidden_paths(files):
@@ -62,8 +62,8 @@ def check_forbidden_paths(files):
62
62
  for forbidden in FORBIDDEN_PATHS:
63
63
  if forbidden in line:
64
64
  violations.append((f, line_no, forbidden))
65
- except Exception:
66
- pass
65
+ except OSError as err:
66
+ sys.stderr.write(f"Notice: Could not read {f}: {err}\n")
67
67
  return violations
68
68
 
69
69
  def check_scripts_clean_pom(files):
@@ -83,8 +83,8 @@ def check_scripts_clean_pom(files):
83
83
  violations.append((f, line_no, "Raw findTestObject is forbidden in Scripts/"))
84
84
  if "//" in line and ("//table" in line or "//input" in line or "//button" in line):
85
85
  violations.append((f, line_no, "Raw XPath is forbidden in Scripts/"))
86
- except Exception:
87
- pass
86
+ except OSError as err:
87
+ sys.stderr.write(f"Notice: Could not parse {f}: {err}\n")
88
88
  return violations
89
89
 
90
90
  def main():
@@ -185,7 +185,7 @@ def load_config(repo_root=None, config_path=None):
185
185
  with open(config_path, "r", encoding="utf-8") as f:
186
186
  return json.load(f)
187
187
  except (OSError, json.JSONDecodeError):
188
- pass
188
+ config_data = {}
189
189
 
190
190
  root = repo_root or REPO_ROOT
191
191
  candidates = [
@@ -198,7 +198,7 @@ def load_config(repo_root=None, config_path=None):
198
198
  with open(c, "r", encoding="utf-8") as f:
199
199
  return json.load(f)
200
200
  except (OSError, json.JSONDecodeError):
201
- pass
201
+ continue
202
202
  return {}
203
203
 
204
204
 
@@ -325,8 +325,8 @@ def classify_file_role(file_path: str) -> str:
325
325
  if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
326
326
  return "PAGE_OBJECT"
327
327
 
328
- # 3. Helpers & Fixtures / Tools & Utilities
329
- if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin") for p in parts):
328
+ # 3. Helpers & Fixtures / Tools & Utilities / Benchmarks
329
+ if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin", "benchmarks", "benchmark") for p in parts):
330
330
  return "HELPER"
331
331
  if any(base.endswith(sfx) for sfx in ("helper.ts", "helper.js", "helper.py", "helper.groovy", "utils.ts", "utils.js", "utils.py", "fixture.ts", "fixture.js")):
332
332
  return "HELPER"
@@ -613,7 +613,12 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
613
613
  for ext in EXTENSIONS:
614
614
  raw_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
615
615
  # Filter out non-test files like tools, __init__.py, node_modules when scanning generic directory
616
- ignored_parts = {"tools", "node_modules", ".git", "__pycache__", "venv", ".venv"} | extra_ignored
616
+ ignored_parts = {
617
+ "tools", "bin", "node_modules", ".git", "__pycache__", "venv", ".venv",
618
+ "claude-ecosystem-repos", "dist", "build", ".idea", ".vscode",
619
+ ".mypy_cache", ".pytest_cache", ".ruff_cache", ".sandbox_snapshots",
620
+ "graphify-out"
621
+ } | extra_ignored
617
622
  script_files = [
618
623
  f for f in raw_files
619
624
  if not any(part.lower() in ignored_parts for part in os.path.relpath(f, scan_dir).replace("\\", "/").split("/")[:-1])
@@ -768,8 +773,8 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
768
773
  os.makedirs(os.path.dirname(target_out), exist_ok=True)
769
774
  with open(target_out, "w", encoding="utf-8") as f:
770
775
  json.dump(report_data, f, indent=2, ensure_ascii=False)
771
- except OSError:
772
- pass
776
+ except OSError as err:
777
+ sys.stderr.write(f"Notice: Target report write failed: {err}\n")
773
778
 
774
779
  if strict:
775
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"))
@@ -55,8 +55,8 @@ def extract_groovy_methods(file_path):
55
55
  mname = m.group(1)
56
56
  if mname not in {"if", "for", "while", "switch", "catch"}:
57
57
  methods.add(mname)
58
- except Exception:
59
- pass
58
+ except (OSError, UnicodeDecodeError) as err:
59
+ sys.stderr.write(f"Notice: Parse methods skipped for {file_path}: {err}\n")
60
60
  return methods
61
61
 
62
62
  def get_actual_entities(repo_root):
@@ -59,8 +59,8 @@ def estimate_test_duration(file_path: str, full_path: str) -> float:
59
59
  conn.close()
60
60
  if row and row[0]:
61
61
  return float(row[0])
62
- except Exception:
63
- pass
62
+ except sqlite3.Error:
63
+ conn = None
64
64
 
65
65
  # Heuristic fallback: ~0.5s per line of script
66
66
  try:
@@ -68,8 +68,8 @@ def estimate_test_duration(file_path: str, full_path: str) -> float:
68
68
  with open(full_path, "r", encoding="utf-8", errors="ignore") as fp:
69
69
  lines = len(fp.readlines())
70
70
  return max(5.0, round(lines * 0.25, 1))
71
- except Exception:
72
- pass
71
+ except (OSError, UnicodeDecodeError):
72
+ lines = 0
73
73
 
74
74
  return 10.0
75
75
 
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
23
23
  except Exception:
24
24
  pass
25
25
 
26
- SERVER_VERSION = "2.4.2"
26
+ SERVER_VERSION = "2.5.0"
27
27
 
28
28
  NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
29
29
  NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"