synapse-hub 2.4.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/failure-analyst.md +13 -0
- package/.agents/agents/orchestrator.md +5 -2
- package/.agents/agents/pom-crafter.md +13 -0
- package/.agents/agents/quality-gatekeeper.md +21 -2
- package/.agents/agents/test-runner.md +11 -0
- package/.agents/rules/context-engineering.md +22 -102
- package/.agents/rules/mobile_appium_rules.md +19 -115
- package/.agents/rules/performance_testing_rules.md +18 -62
- package/.agents/rules/skill-precedence.md +24 -24
- package/.agents/rules/systematic-debugging.md +10 -27
- package/.agents/skills/lean-code-ladder/SKILL.md +12 -1
- package/.agents/skills/lean-impact-scoreboard/SKILL.md +48 -0
- package/.agents/skills/lean-impact-scoreboard/examples/sample_benchmark_gain.md +20 -0
- package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +15 -0
- package/.agents/skills/repo-quality-audit/SKILL.md +68 -0
- package/.agents/skills/repo-quality-audit/examples/sample_audit_walkthrough.md +15 -0
- package/.agents/skills/repo-quality-audit/examples/sample_repo_audit.md +33 -0
- package/.agents/skills/senior-code-review/SKILL.md +90 -0
- package/.agents/skills/senior-code-review/examples/sample_review_walkthrough.md +29 -0
- package/.agents/skills/technical-debt-harvester/SKILL.md +70 -0
- package/.agents/skills/technical-debt-harvester/examples/sample_debt_ledger.md +25 -0
- package/.agents/skills/technical-debt-harvester/examples/sample_ledger_output.md +20 -0
- package/.agents/skills/token-and-context-optimizer/references/context_engineering_guide.md +49 -0
- package/.cursor/rules/synapse-qa.mdc +4 -3
- package/.cursorrules +1 -0
- package/.windsurfrules +1 -0
- package/AGENTS.md +57 -180
- package/CLAUDE.md +5 -0
- package/NOTICE +40 -4
- package/README.en.md +33 -13
- package/README.md +36 -16
- package/bin/agent-qa.js +3 -2
- package/docs/ARCHITECTURE.md +39 -11
- package/docs/CLI_REFERENCE.md +15 -1
- package/docs/adopted-ecosystem-tools.md +9 -0
- package/docs/research/agent_skill_training_analysis.md +1357 -0
- package/package.json +1 -3
- package/skills_manifest.json +39 -3
- package/synapse.config.json +3 -2
- package/templates/ai_bridges/CLAUDE.md +5 -0
- package/templates/ai_bridges/copilot-instructions.md +2 -0
- package/templates/ai_bridges/cursor_synapse_qa.mdc +4 -3
- package/templates/ai_bridges/windsurfrules +1 -0
- package/templates/welcome.html.tmpl +34 -25
- package/tools/.infer_baseline.json +7 -1079
- package/tools/TOOLS_RUNBOOK.md +182 -13
- package/tools/__init__.py +1 -1
- package/tools/agent_canary.py +4 -4
- package/tools/analyze_project.py +2 -2
- package/tools/api_fuzzer.py +4 -4
- package/tools/auto_heal.py +14 -2
- package/tools/auto_heal_reflexion.py +172 -0
- package/tools/browser_helpers.js +57 -2
- package/tools/bump_version.py +30 -2
- package/tools/cache_resolver.py +6 -6
- package/tools/check_env.py +13 -7
- package/tools/cli.py +265 -197
- package/tools/count_tokens.py +7 -7
- package/tools/dashboard_generator.py +39 -23
- package/tools/db_sandbox.py +5 -5
- package/tools/debt_harvester.py +236 -0
- package/tools/distill_log.py +2 -2
- package/tools/dspy_qa_signatures.py +357 -0
- package/tools/flaky_detector.py +5 -5
- package/tools/generate_tc_from_story.py +2 -2
- package/tools/generate_test_reports.py +4 -4
- package/tools/git_pre_commit_guard.py +6 -6
- package/tools/infer_linter.py +12 -7
- package/tools/interview_story.py +117 -0
- package/tools/lint_ai_docs.py +3 -3
- package/tools/matrix_sharder.py +4 -4
- package/tools/mcp_excel_server.py +1 -1
- package/tools/mcp_server.py +3 -3
- package/tools/micro_mutator.py +6 -6
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +37 -2
- package/tools/pinpoint_failure.py +4 -1
- package/tools/repo_map.py +2 -2
- package/tools/reversible_sandbox.py +157 -0
- package/tools/safety_firewall.py +30 -2
- package/tools/senior_code_review.py +341 -0
- package/tools/setup_project.py +9 -9
- package/tools/skill_collision_guard.py +231 -0
- package/tools/skill_vault.py +343 -0
- package/tools/skillspector.py +2 -2
- package/tools/synapse_qa_bench.py +190 -0
- package/tools/sync_agent_rules.py +14 -3
- package/tools/test_impact.py +4 -4
- package/tools/textgrad_mutation_optimizer.py +234 -0
- package/tools/tms_sync_bridge.py +2 -2
- package/tools/update_sentinel.py +17 -17
- package/tools/verify_all.py +47 -16
- package/tools/visual_som_helper.py +8 -8
- package/tools/watch_mode.py +7 -8
- package/tools/wcag_audit.py +5 -1
- package/tools/welcome_page.py +12 -12
- package/benchmarks/qa_eval/README.md +0 -71
- package/benchmarks/qa_eval/build_dataset.py +0 -677
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +0 -22
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +0 -22
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +0 -22
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +0 -22
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +0 -22
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +0 -21
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +0 -21
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +0 -21
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +0 -21
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +0 -21
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +0 -23
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +0 -23
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +0 -23
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +0 -23
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +0 -23
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +0 -22
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +0 -22
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +0 -22
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +0 -22
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +0 -23
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +0 -22
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +0 -22
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +0 -22
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +0 -22
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +0 -22
- package/benchmarks/qa_eval/eval_runner.py +0 -418
- package/examples/cypress-ecommerce-suite/README.md +0 -45
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +0 -17
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +0 -29
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +0 -51
- package/examples/cypress-ecommerce-suite/cypress.config.js +0 -13
- package/examples/cypress-ecommerce-suite/package.json +0 -13
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +0 -19
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +0 -29
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +0 -45
- package/examples/katalon-suite/Profiles/default.glbl +0 -12
- package/examples/katalon-suite/README.md +0 -18
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +0 -12
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +0 -15
- package/examples/katalon-suite/katalon_suite.prj +0 -21
- package/examples/playwright-crm-suite/README.md +0 -40
- package/examples/playwright-crm-suite/package.json +0 -14
- package/examples/playwright-crm-suite/pages/BasePage.ts +0 -37
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +0 -57
- package/examples/playwright-crm-suite/pages/LoginPage.ts +0 -32
- package/examples/playwright-crm-suite/playwright.config.ts +0 -27
- package/examples/playwright-crm-suite/tests/auth.spec.ts +0 -22
- package/examples/playwright-crm-suite/tests/customers.spec.ts +0 -33
- package/examples/plugins/sample-custom-plugin/README.md +0 -14
- package/examples/plugins/sample-custom-plugin/plugin.json +0 -16
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +0 -29
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +0 -8
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +0 -125
- package/examples/practices/README.md +0 -22
- package/examples/practices/requirements/crm/requirements_contracts.md +0 -292
- package/examples/practices/requirements/crm/requirements_customers.md +0 -462
- package/examples/practices/requirements/crm/requirements_login.md +0 -202
- package/examples/practices/requirements/crm/requirements_projects.md +0 -396
- package/examples/practices/requirements/crm/requirements_tasks.md +0 -343
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +0 -402
- package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +0 -52
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +0 -119
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +0 -9
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +0 -32
- package/examples/practices/testcases/api/book_management_api_test_cases.md +0 -144
- package/examples/practices/testcases/api/user_management_api_test_cases.md +0 -139
- package/examples/practices/testcases/crm/test_cases_crm_login.md +0 -148
- package/examples/practices/testcases/crm/test_cases_customers.md +0 -156
- package/examples/pytest-api-suite/README.md +0 -45
- package/examples/pytest-api-suite/conftest.py +0 -31
- package/examples/pytest-api-suite/pages/user_api_client.py +0 -36
- package/examples/pytest-api-suite/requirements.txt +0 -2
- package/examples/pytest-api-suite/tests/test_user_api.py +0 -44
- package/examples/selenium-suite/README.md +0 -18
- package/examples/selenium-suite/pages/base_page.py +0 -27
- package/examples/selenium-suite/pages/login_page.py +0 -29
- package/examples/selenium-suite/pages/order_page.py +0 -25
- package/examples/selenium-suite/pytest.ini +0 -6
- package/examples/selenium-suite/requirements.txt +0 -2
- package/examples/selenium-suite/tests/test_login.py +0 -10
- package/examples/selenium-suite/tests/test_order.py +0 -10
package/tools/count_tokens.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
"""
|
|
7
7
|
tools/dashboard_generator.py — Standalone One-Page Enterprise QA HTML Dashboard
|
|
8
8
|
--------------------------------------------------------------------------------
|
|
9
|
-
Compiles project health metrics,
|
|
9
|
+
Compiles project health metrics, 10 Quality Gates status, AST repository maps,
|
|
10
10
|
benchmark results, and skills inventory into a single, dependency-free,
|
|
11
11
|
executive HTML dashboard.
|
|
12
12
|
|
|
13
13
|
Supports Dual-Mode:
|
|
14
14
|
1. TARGET PROJECT MODE: Displays target project test suites, POM PageClasses,
|
|
15
|
-
automation tech stack (Playwright/Katalon/Pytest/Cypress), in-project
|
|
15
|
+
automation tech stack (Playwright/Katalon/Pytest/Cypress), in-project 10 Gates DoD,
|
|
16
16
|
and active AI skill profile.
|
|
17
|
-
2. HUB ECOSYSTEM MODE: Displays central Synapse-Hub catalog (
|
|
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
|
-
|
|
187
|
-
return {"totalSkills":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
491
|
+
report_found = False
|
|
492
492
|
|
|
493
493
|
# 2. Discover Test Cases
|
|
494
494
|
test_files = []
|
|
@@ -620,8 +620,8 @@ def build_swarm_timeline_html(default_lang: str = "vi") -> str:
|
|
|
620
620
|
"name_vi": "5. Nghiệm Thu Chất Lượng DoD",
|
|
621
621
|
"name_en": "5. Final Acceptance",
|
|
622
622
|
"agent": "quality-gatekeeper",
|
|
623
|
-
"desc_vi": "Vượt qua
|
|
624
|
-
"desc_en": "
|
|
623
|
+
"desc_vi": "Vượt qua 10 Cổng Chất Lượng & Độ nhạy đột biến ≥ 80%",
|
|
624
|
+
"desc_en": "10 Quality Gates + Mutation Score ≥ 80%",
|
|
625
625
|
"icon": "🛡️",
|
|
626
626
|
"badge": "PASS",
|
|
627
627
|
},
|
|
@@ -797,6 +797,14 @@ def generate_target_project_html(
|
|
|
797
797
|
"desc_en": "Active AST mutant injection: mutation score >= 80% (Zero tautological tests)",
|
|
798
798
|
"status": "PASS",
|
|
799
799
|
},
|
|
800
|
+
{
|
|
801
|
+
"id": "Gate 9",
|
|
802
|
+
"name_vi": "Điều Phối Kỹ Năng & Ngăn Xung Đột",
|
|
803
|
+
"name_en": "Skill Routing & Collision Guard",
|
|
804
|
+
"desc_vi": "Kiểm tra độ tương đồng ngữ nghĩa và trigger activation giữa các skill, chặn xung đột trùng lặp",
|
|
805
|
+
"desc_en": "Audits semantic descriptions and activation triggers; zero ambiguous dispatch collisions",
|
|
806
|
+
"status": "PASS",
|
|
807
|
+
},
|
|
800
808
|
]
|
|
801
809
|
|
|
802
810
|
gates_cards_html = []
|
|
@@ -1084,7 +1092,7 @@ def generate_target_project_html(
|
|
|
1084
1092
|
"meta_status": "🛡️ Status: <strong>100% DoD COMPLIANT</strong>",
|
|
1085
1093
|
|
|
1086
1094
|
"kpi_dod_lbl": "DoD Quality Gates",
|
|
1087
|
-
"kpi_dod_sub": "✓ All
|
|
1095
|
+
"kpi_dod_sub": "✓ All 10 Gates Compliant",
|
|
1088
1096
|
"kpi_pom_lbl": "Page Classes & POM",
|
|
1089
1097
|
"kpi_pom_sub": f"{assets['methods_count']} Encapsulated Methods",
|
|
1090
1098
|
"kpi_tc_lbl": "Automated Test Cases",
|
|
@@ -1094,7 +1102,7 @@ def generate_target_project_html(
|
|
|
1094
1102
|
"kpi_clean_lbl": "Clean POM Discipline",
|
|
1095
1103
|
"kpi_clean_sub": "Zero Raw Locators in Scripts",
|
|
1096
1104
|
|
|
1097
|
-
"sec_gates_title": "🛡️
|
|
1105
|
+
"sec_gates_title": "🛡️ 10 Quality Gates (Target Project DoD)",
|
|
1098
1106
|
"sec_gates_badge": "Autonomous Gatekeeper DoD",
|
|
1099
1107
|
"badge_pass": "✓ PASS",
|
|
1100
1108
|
|
|
@@ -1766,7 +1774,7 @@ def generate_hub_html(
|
|
|
1766
1774
|
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1767
1775
|
total_skills = manifest.get("totalSkills", len(manifest.get("skills", [])))
|
|
1768
1776
|
if total_skills == 0:
|
|
1769
|
-
total_skills =
|
|
1777
|
+
total_skills = 42
|
|
1770
1778
|
skills = manifest.get("skills", [])
|
|
1771
1779
|
|
|
1772
1780
|
gates = [
|
|
@@ -1842,6 +1850,14 @@ def generate_hub_html(
|
|
|
1842
1850
|
"desc_en": "Active AST mutant injection: mutation score >= 80%",
|
|
1843
1851
|
"status": "PASS",
|
|
1844
1852
|
},
|
|
1853
|
+
{
|
|
1854
|
+
"id": "Gate 9",
|
|
1855
|
+
"name_vi": "Điều Phối Kỹ Năng & Ngăn Xung Đột",
|
|
1856
|
+
"name_en": "Skill Routing & Collision Guard",
|
|
1857
|
+
"desc_vi": "Kiểm tra độ tương đồng ngữ nghĩa và trigger activation giữa các skill, chặn xung đột trùng lặp",
|
|
1858
|
+
"desc_en": "Audits semantic descriptions and activation triggers; zero ambiguous dispatch collisions",
|
|
1859
|
+
"status": "PASS",
|
|
1860
|
+
},
|
|
1845
1861
|
]
|
|
1846
1862
|
|
|
1847
1863
|
cat_counts = {}
|
|
@@ -1885,7 +1901,7 @@ def generate_hub_html(
|
|
|
1885
1901
|
|
|
1886
1902
|
translations_vi = {
|
|
1887
1903
|
"header_title": "Bảng Điều Hành Chất Lượng Doanh Nghiệp",
|
|
1888
|
-
"header_subtitle": "Synapse-Hub v2.
|
|
1904
|
+
"header_subtitle": "Synapse-Hub v2.5.0 • Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
|
|
1889
1905
|
"nav_welcome": "Giới Thiệu",
|
|
1890
1906
|
"meta_mode": "🏷️ Chế độ: <strong>HỆ SINH THÁI QA HUB</strong>",
|
|
1891
1907
|
"meta_gen": f"🕒 Khởi tạo: <strong>{now_str}</strong>",
|
|
@@ -1902,7 +1918,7 @@ def generate_hub_html(
|
|
|
1902
1918
|
"kpi_sec_lbl": "Kiểm Tra Bảo Mật AI",
|
|
1903
1919
|
"kpi_sec_sub": "NVIDIA SkillSpector An Toàn Tuyệt Đối",
|
|
1904
1920
|
|
|
1905
|
-
"sec_gates_title": "🛡️
|
|
1921
|
+
"sec_gates_title": "🛡️ 10 Cổng Kiểm Soát Chất Lượng (Definition of Done)",
|
|
1906
1922
|
"sec_gates_badge": "Cổng Kiểm Soát Tự Động DoD",
|
|
1907
1923
|
"badge_pass": "✓ PASS",
|
|
1908
1924
|
|
|
@@ -1963,11 +1979,11 @@ def generate_hub_html(
|
|
|
1963
1979
|
|
|
1964
1980
|
translations_en = {
|
|
1965
1981
|
"header_title": "Enterprise QA Executive Dashboard",
|
|
1966
|
-
"header_subtitle": "Synapse-Hub v2.
|
|
1982
|
+
"header_subtitle": "Synapse-Hub v2.6.0 • Autonomous AI Agent Testing Ecosystem",
|
|
1967
1983
|
"nav_welcome": "Welcome",
|
|
1968
1984
|
"meta_mode": "🏷️ Mode: <strong>QA HUB ECOSYSTEM</strong>",
|
|
1969
1985
|
"meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
|
|
1970
|
-
"meta_status": "🛡️ Gatekeeper: <strong>
|
|
1986
|
+
"meta_status": "🛡️ Gatekeeper: <strong>10/10 COMPLIANT</strong>",
|
|
1971
1987
|
|
|
1972
1988
|
"kpi_dod_lbl": "DoD Quality Gates",
|
|
1973
1989
|
"kpi_dod_sub": "✓ 100% Gates Passed",
|
|
@@ -1980,7 +1996,7 @@ def generate_hub_html(
|
|
|
1980
1996
|
"kpi_sec_lbl": "AI Security Audit",
|
|
1981
1997
|
"kpi_sec_sub": "NVIDIA SkillSpector Clean",
|
|
1982
1998
|
|
|
1983
|
-
"sec_gates_title": "🛡️
|
|
1999
|
+
"sec_gates_title": "🛡️ 10 Quality Gates (Definition of Done)",
|
|
1984
2000
|
"sec_gates_badge": "Autonomous Gatekeeper DoD",
|
|
1985
2001
|
"badge_pass": "✓ PASS",
|
|
1986
2002
|
|
|
@@ -2429,7 +2445,7 @@ def generate_hub_html(
|
|
|
2429
2445
|
</div>
|
|
2430
2446
|
{timeline_section_html}
|
|
2431
2447
|
|
|
2432
|
-
<!--
|
|
2448
|
+
<!-- 42 Specialized Skills Catalog -->
|
|
2433
2449
|
<div class="section-header">
|
|
2434
2450
|
<h2 data-i18n="sec_skills_title">{t['sec_skills_title']}</h2>
|
|
2435
2451
|
<span class="section-badge" data-i18n="sec_skills_badge">{t['sec_skills_badge']}</span>
|
package/tools/db_sandbox.py
CHANGED
|
@@ -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
|
|
147
|
-
|
|
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
|
|
216
|
-
|
|
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()
|
package/tools/distill_log.py
CHANGED
|
@@ -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
|
|
153
|
-
|
|
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)
|