synapse-hub 2.2.4 → 2.4.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 +11 -0
- package/.agents/agents/live-explorer.md +28 -0
- package/.agents/agents/orchestrator.md +20 -4
- package/.agents/agents/pom-crafter.md +12 -0
- package/.agents/agents/quality-gatekeeper.md +19 -4
- package/.agents/agents/test-runner.md +11 -0
- package/.agents/rules/communication-style.md +40 -0
- package/.agents/rules/context-engineering.md +23 -0
- package/.agents/rules/git-push-protocol.md +10 -16
- package/.agents/rules/graphify.md +14 -14
- package/.agents/rules/mandatory-playwright-live.md +29 -29
- package/.agents/rules/mobile_appium_rules.md +132 -0
- package/.agents/rules/performance_testing_rules.md +74 -0
- package/.agents/rules/systematic-debugging.md +6 -6
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
- package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
- package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
- package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
- package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
- package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
- package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
- package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
- package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
- package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
- package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
- package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
- package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
- package/.agents/skills/github-actions-templates/SKILL.md +47 -304
- package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
- package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
- package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
- package/.agents/skills/lam-script-generator/SKILL.md +42 -1
- package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
- package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
- package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
- package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
- package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
- package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
- package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
- package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
- package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
- package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
- package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
- package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
- package/.agents/skills/self-healing-locator/SKILL.md +24 -10
- package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
- package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
- package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
- package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
- package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
- package/.agents/skills/systematic-debugging/SKILL.md +100 -0
- package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
- package/.agents/skills/test-driven-development/SKILL.md +114 -0
- package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
- package/.agents/skills/verification-before-completion/SKILL.md +100 -0
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
- package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
- package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
- package/.agents/templates/completion_report.md +57 -57
- package/.agents/workflows/analyze_requirement_document.md +18 -0
- package/.agents/workflows/export_testcases_excel.md +32 -0
- package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
- package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
- package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
- package/.agents/workflows/generate_requirements_from_website.md +25 -0
- package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
- package/.agents/workflows/graphify.md +10 -10
- package/.agents/workflows/verify.md +2 -2
- package/.cursor/rules/language.mdc +7 -7
- package/.cursor/rules/synapse-qa.mdc +23 -0
- package/.cursorrules +7 -0
- package/.pre-commit-hooks.yaml +14 -0
- package/.windsurfrules +7 -0
- package/AGENTS.md +54 -15
- package/CLAUDE.md +35 -12
- package/LICENSE +30 -0
- package/NOTICE +180 -0
- package/README.en.md +325 -0
- package/README.md +247 -516
- package/action.yml +51 -0
- package/benchmarks/qa_eval/README.md +71 -0
- package/benchmarks/qa_eval/build_dataset.py +677 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/eval_runner.py +418 -0
- package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
- package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
- package/benchmarks/qa_eval/reports/eval_results.json +105 -0
- package/bin/agent-qa.js +10 -3
- package/docs/ARCHITECTURE.md +107 -0
- package/docs/CI_CD_INTEGRATION.md +94 -0
- package/docs/CLI_REFERENCE.md +157 -0
- package/docs/CONTRIBUTING.md +91 -0
- package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
- package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
- package/docs/adopted-ecosystem-tools.md +87 -0
- package/docs/schemas/synapse.config.schema.json +64 -0
- package/examples/cypress-ecommerce-suite/README.md +45 -0
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
- package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
- package/examples/cypress-ecommerce-suite/package.json +13 -0
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
- package/examples/katalon-suite/Profiles/default.glbl +12 -0
- package/examples/katalon-suite/README.md +18 -0
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
- package/examples/katalon-suite/katalon_suite.prj +21 -0
- package/examples/playwright-crm-suite/README.md +40 -0
- package/examples/playwright-crm-suite/package.json +14 -0
- package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
- package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
- package/examples/playwright-crm-suite/playwright.config.ts +27 -0
- package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
- package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
- package/examples/plugins/sample-custom-plugin/README.md +14 -0
- package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
- package/examples/practices/README.md +22 -0
- package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
- package/examples/practices/requirements/crm/requirements_customers.md +462 -0
- package/examples/practices/requirements/crm/requirements_login.md +202 -0
- package/examples/practices/requirements/crm/requirements_projects.md +396 -0
- package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
- 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 +52 -0
- 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 +119 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
- 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 +32 -0
- package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
- package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
- package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
- package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
- package/examples/pytest-api-suite/README.md +45 -0
- package/examples/pytest-api-suite/conftest.py +31 -0
- package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
- package/examples/pytest-api-suite/requirements.txt +2 -0
- package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
- package/examples/selenium-suite/README.md +18 -0
- package/examples/selenium-suite/pages/base_page.py +27 -0
- package/examples/selenium-suite/pages/login_page.py +29 -0
- package/examples/selenium-suite/pages/order_page.py +25 -0
- package/examples/selenium-suite/pytest.ini +6 -0
- package/examples/selenium-suite/requirements.txt +2 -0
- package/examples/selenium-suite/tests/test_login.py +10 -0
- package/examples/selenium-suite/tests/test_order.py +10 -0
- package/package.json +22 -7
- package/scripts/generate_skill_examples.py +244 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install_global_plugin.ps1 +94 -0
- package/scripts/install_global_plugin.sh +49 -0
- package/scripts/lint_skills.ps1 +57 -0
- package/scripts/package_dist.py +323 -0
- package/scripts/setup.sh +59 -0
- package/scripts/update_hub.ps1 +19 -0
- package/scripts/update_hub.sh +15 -0
- package/skills_manifest.json +209 -6
- package/synapse.config.json +30 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
- package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
- package/templates/ai_bridges/CLAUDE.md +33 -16
- package/templates/ai_bridges/copilot-instructions.md +14 -21
- package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
- package/templates/ai_bridges/windsurfrules +21 -21
- package/templates/ci/github-actions-synapse.yml +71 -4
- package/templates/welcome.html.tmpl +1674 -0
- package/tools/.infer_baseline.json +1102 -0
- package/tools/TOOLS_RUNBOOK.md +92 -130
- package/tools/__init__.py +26 -0
- package/tools/agent_canary.py +501 -501
- package/tools/analyze_project.py +31 -17
- package/tools/api_fuzzer.py +278 -6
- package/tools/author_integrity.py +6 -6
- package/tools/auto_heal.py +1 -1
- package/tools/browser_helpers.js +105 -7
- package/tools/bug_reporter.py +31 -1
- package/tools/bump_version.py +23 -3
- package/tools/cache_resolver.py +248 -14
- package/tools/check_env.py +55 -14
- package/tools/check_version_sync.py +153 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +263 -13
- package/tools/count_tokens.py +1257 -0
- package/tools/dashboard_generator.py +2638 -0
- package/tools/db_sandbox.py +198 -20
- package/tools/e2e_flow_prober.ps1 +12 -64
- package/tools/e2e_flow_prober.py +99 -0
- package/tools/e2e_flow_prober.sh +4 -0
- package/tools/flaky_detector.py +4 -3
- package/tools/generate_allure_report.ps1 +12 -29
- package/tools/generate_allure_report.py +77 -0
- package/tools/generate_allure_report.sh +4 -0
- package/tools/generate_jmx_plan.py +257 -58
- package/tools/generate_test_reports.ps1 +12 -134
- package/tools/generate_test_reports.py +133 -0
- package/tools/generate_test_reports.sh +4 -0
- package/tools/gherkin_suite.py +758 -0
- package/tools/git_pre_commit_guard.py +12 -6
- package/tools/goldfish_review.py +366 -0
- package/tools/heal_pr_automator.py +1 -1
- package/tools/heuristic_locator_matcher.py +170 -21
- package/tools/infer_linter.py +612 -70
- package/tools/lint_ai_docs.py +64 -0
- package/tools/matrix_sharder.py +1 -1
- package/tools/mcp_excel_server.ps1 +12 -250
- package/tools/mcp_excel_server.py +249 -0
- package/tools/mcp_excel_server.sh +4 -0
- package/tools/mcp_server.py +368 -144
- package/tools/md_to_xlsx.py +307 -0
- package/tools/micro_mutator.py +300 -69
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +128 -0
- package/tools/pinpoint_failure.py +88 -88
- package/tools/project_state.py +2 -2
- package/tools/repo_map.py +1 -1
- package/tools/safety_firewall.py +196 -45
- package/tools/setup_project.py +294 -30
- package/tools/spec_drift_sentinel.py +439 -0
- package/tools/sync_agent_rules.py +248 -0
- package/tools/telemetry_correlator.py +45 -0
- package/tools/test_impact.py +51 -12
- package/tools/tms_sync_bridge.py +21 -5
- package/tools/universal_ir.py +3 -0
- package/tools/update_sentinel.py +87 -15
- package/tools/verify_all.ps1 +13 -0
- package/tools/verify_all.py +463 -79
- package/tools/visual_diff_canvas.py +1 -0
- package/tools/visual_som_helper.py +1 -1
- package/tools/watch_mode.py +305 -0
- package/tools/welcome_page.py +291 -0
|
@@ -219,6 +219,12 @@ def run_correlation_analysis(
|
|
|
219
219
|
"db_pool_correlation": round(corr_db, 3),
|
|
220
220
|
"disk_correlation": round(corr_disk, 3)
|
|
221
221
|
},
|
|
222
|
+
"otel_metrics": {
|
|
223
|
+
"http.server.request.duration.p95_ms": round(max_p95, 2),
|
|
224
|
+
"system.cpu.utilization_ratio": round(max_cpu / 100.0, 4),
|
|
225
|
+
"system.memory.utilization_ratio": round(max_mem / 100.0, 4),
|
|
226
|
+
"system.db_pool.utilization_ratio": round(max_db / 100.0, 4),
|
|
227
|
+
},
|
|
222
228
|
"root_causes": root_causes
|
|
223
229
|
}
|
|
224
230
|
|
|
@@ -274,6 +280,7 @@ def main():
|
|
|
274
280
|
parser.add_argument("--server-metrics", "-s", help="Path to Server Telemetry CSV (timestamp, cpu_pct, memory_pct, db_pool_pct).")
|
|
275
281
|
parser.add_argument("--markdown", "-m", help="Path to export Markdown RCA report.")
|
|
276
282
|
parser.add_argument("--json", "-j", help="Path to export structured JSON diagnostics.")
|
|
283
|
+
parser.add_argument("--otel", action="store_true", help="Export OpenTelemetry ResourceMetrics payload format.")
|
|
277
284
|
parser.add_argument("--sample", action="store_true", help="Generate synthetic client & server telemetry dataset for verification.")
|
|
278
285
|
|
|
279
286
|
args = parser.parse_args()
|
|
@@ -333,6 +340,44 @@ def main():
|
|
|
333
340
|
print(f" 👉 Recommendation: {c['recommendation']}")
|
|
334
341
|
print("=" * 80 + "\n")
|
|
335
342
|
|
|
343
|
+
if args.otel:
|
|
344
|
+
otel_metrics = {
|
|
345
|
+
"resourceMetrics": [
|
|
346
|
+
{
|
|
347
|
+
"resource": {
|
|
348
|
+
"attributes": [
|
|
349
|
+
{"key": "service.name", "value": {"stringValue": "synapse-backend-under-test"}},
|
|
350
|
+
{"key": "telemetry.sdk.name", "value": {"stringValue": "synapse-correlator"}},
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
"scopeMetrics": [
|
|
354
|
+
{
|
|
355
|
+
"scope": {"name": "synapse.telemetry.correlator", "version": "1.0.0"},
|
|
356
|
+
"metrics": [
|
|
357
|
+
{
|
|
358
|
+
"name": "http.server.request.duration",
|
|
359
|
+
"unit": "ms",
|
|
360
|
+
"gauge": {"dataPoints": [{"value": analysis["metrics_summary"]["max_p95_ms"]}]}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"name": "system.cpu.utilization",
|
|
364
|
+
"unit": "1",
|
|
365
|
+
"gauge": {"dataPoints": [{"value": round(analysis["metrics_summary"]["max_cpu_pct"] / 100.0, 4)}]}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "system.memory.utilization",
|
|
369
|
+
"unit": "1",
|
|
370
|
+
"gauge": {"dataPoints": [{"value": round(analysis["metrics_summary"]["max_memory_pct"] / 100.0, 4)}]}
|
|
371
|
+
},
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
print(json.dumps(otel_metrics, indent=2))
|
|
379
|
+
return
|
|
380
|
+
|
|
336
381
|
if args.markdown:
|
|
337
382
|
md = format_rca_markdown(analysis)
|
|
338
383
|
os.makedirs(os.path.dirname(os.path.abspath(args.markdown)) or ".", exist_ok=True)
|
package/tools/test_impact.py
CHANGED
|
@@ -16,6 +16,7 @@ import os
|
|
|
16
16
|
import sys
|
|
17
17
|
import json
|
|
18
18
|
import glob
|
|
19
|
+
import re
|
|
19
20
|
import argparse
|
|
20
21
|
import subprocess
|
|
21
22
|
|
|
@@ -84,7 +85,7 @@ def build_fallback_graph(repo_root):
|
|
|
84
85
|
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
|
|
85
86
|
content = fp.read()
|
|
86
87
|
for other_cls in class_map:
|
|
87
|
-
if other_cls != cls_name and other_cls
|
|
88
|
+
if other_cls != cls_name and re.search(r"\b" + re.escape(other_cls) + r"\b", content):
|
|
88
89
|
edges.append({"source": cls_name, "target": other_cls})
|
|
89
90
|
except Exception:
|
|
90
91
|
pass
|
|
@@ -112,7 +113,9 @@ def build_fallback_graph(repo_root):
|
|
|
112
113
|
with open(f, "r", encoding="utf-8", errors="ignore") as fp:
|
|
113
114
|
content = fp.read()
|
|
114
115
|
for cls_name in class_map:
|
|
115
|
-
|
|
116
|
+
# Word-boundary match: "BorrowPage" must not hit inside
|
|
117
|
+
# "ApprovalBorrowPage" (substring match over-counted).
|
|
118
|
+
if re.search(r"\b" + re.escape(cls_name) + r"\b", content):
|
|
116
119
|
edges.append({"source": rel_path, "target": cls_name})
|
|
117
120
|
except Exception:
|
|
118
121
|
pass
|
|
@@ -120,20 +123,48 @@ def build_fallback_graph(repo_root):
|
|
|
120
123
|
return {"nodes": nodes, "edges": edges}
|
|
121
124
|
|
|
122
125
|
|
|
126
|
+
def merge_graphs(primary, fallback):
|
|
127
|
+
"""Unions two graph dicts (nodes by id, edges by (source, target)).
|
|
128
|
+
|
|
129
|
+
The Graphify graph can silently miss edges (its Groovy extractor fails on
|
|
130
|
+
GString/closure-heavy files), so the grep-based fallback graph is always
|
|
131
|
+
merged in. Over-inclusion is the safe direction for impact analysis.
|
|
132
|
+
"""
|
|
133
|
+
merged = {"nodes": [], "edges": []}
|
|
134
|
+
seen_nodes, seen_edges = set(), set()
|
|
135
|
+
for node in list(primary.get("nodes", [])) + list(fallback.get("nodes", [])):
|
|
136
|
+
node_id = node.get("id")
|
|
137
|
+
if node_id and node_id not in seen_nodes:
|
|
138
|
+
seen_nodes.add(node_id)
|
|
139
|
+
merged["nodes"].append(node)
|
|
140
|
+
for edge in list(primary.get("edges", [])) + list(fallback.get("edges", [])):
|
|
141
|
+
key = (edge.get("source"), edge.get("target"))
|
|
142
|
+
if key not in seen_edges:
|
|
143
|
+
seen_edges.add(key)
|
|
144
|
+
merged["edges"].append(edge)
|
|
145
|
+
return merged
|
|
146
|
+
|
|
147
|
+
|
|
123
148
|
def load_graph(repo_root=None):
|
|
124
149
|
root = repo_root or HUB_ROOT
|
|
150
|
+
graph_data = None
|
|
125
151
|
graph_path = os.path.join(root, "graphify-out", "graph.json")
|
|
126
152
|
if os.path.exists(graph_path):
|
|
127
153
|
with open(graph_path, "r", encoding="utf-8") as fp:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
|
|
154
|
+
graph_data = json.load(fp)
|
|
155
|
+
else:
|
|
156
|
+
hub_graph = os.path.join(HUB_ROOT, "graphify-out", "graph.json")
|
|
157
|
+
if os.path.exists(hub_graph):
|
|
158
|
+
with open(hub_graph, "r", encoding="utf-8") as fp:
|
|
159
|
+
graph_data = json.load(fp)
|
|
160
|
+
|
|
161
|
+
# Always union the ground-truth fallback graph: when the target repo has a
|
|
162
|
+
# (possibly incomplete) graphify-out/graph.json the grep scan below is the
|
|
163
|
+
# only thing guaranteeing script->class edges actually exist.
|
|
164
|
+
fallback = build_fallback_graph(root)
|
|
165
|
+
if graph_data is None:
|
|
166
|
+
return fallback
|
|
167
|
+
return merge_graphs(graph_data, fallback)
|
|
137
168
|
|
|
138
169
|
|
|
139
170
|
def get_git_modified_files(repo_root=None):
|
|
@@ -175,7 +206,15 @@ def get_total_test_cases(workspace_dir=None, graph_data=None, override_total=Non
|
|
|
175
206
|
for pat in test_patterns:
|
|
176
207
|
files.update(glob.glob(pat, recursive=True))
|
|
177
208
|
if files:
|
|
178
|
-
|
|
209
|
+
# Katalon-style repos list one TC twice (Scripts/<TC_x>/ScriptN.groovy
|
|
210
|
+
# AND Test Cases/.../TC_x.tc) — dedupe by the TC identifier so the
|
|
211
|
+
# denominator counts logical test cases, not file copies.
|
|
212
|
+
identities = set()
|
|
213
|
+
for f in files:
|
|
214
|
+
rel = os.path.relpath(f, root).replace("\\", "/")
|
|
215
|
+
match = re.search(r"TC[_-]\d+", rel)
|
|
216
|
+
identities.add(("tc", match.group(0)) if match else ("file", rel))
|
|
217
|
+
return len(identities)
|
|
179
218
|
|
|
180
219
|
# 2. Count test case nodes in graph if available
|
|
181
220
|
if graph_data and "nodes" in graph_data:
|
package/tools/tms_sync_bridge.py
CHANGED
|
@@ -44,10 +44,21 @@ class TMSBridge:
|
|
|
44
44
|
|
|
45
45
|
def __init__(self, provider: str = "jira", base_url: str = "", api_token: str = "", user_email: str = ""):
|
|
46
46
|
self.provider = provider.lower()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
resolved_url = base_url or os.getenv("JIRA_URL") or os.getenv("TESTRAIL_URL") or ""
|
|
48
|
+
resolved_token = api_token or os.getenv("JIRA_API_TOKEN") or os.getenv("TESTRAIL_API_KEY") or ""
|
|
49
|
+
resolved_email = user_email or os.getenv("JIRA_USER_EMAIL") or ""
|
|
50
|
+
|
|
51
|
+
# Only use mock simulation if URL is omitted, mock is specified, or token is absent
|
|
52
|
+
if not resolved_url or "mock" in resolved_url or not resolved_token:
|
|
53
|
+
self.base_url = "https://mock-alm.enterprise.local"
|
|
54
|
+
self.api_token = "mock_token_12345"
|
|
55
|
+
self.user_email = resolved_email or "qa-agent@enterprise.local"
|
|
56
|
+
self.is_mock = True
|
|
57
|
+
else:
|
|
58
|
+
self.base_url = resolved_url
|
|
59
|
+
self.api_token = resolved_token
|
|
60
|
+
self.user_email = resolved_email
|
|
61
|
+
self.is_mock = False
|
|
51
62
|
|
|
52
63
|
# -------------------------------------------------------------------------
|
|
53
64
|
# 1. PULL MODE: Extract User Story & Acceptance Criteria
|
|
@@ -173,12 +184,17 @@ class TMSBridge:
|
|
|
173
184
|
"timestamp": datetime.now().isoformat()
|
|
174
185
|
},
|
|
175
186
|
"status": "Created",
|
|
176
|
-
"mode": "MOCK_SIMULATION" if self.is_mock else "
|
|
187
|
+
"mode": "MOCK_SIMULATION" if self.is_mock else f"LIVE_{self.provider.upper()}"
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
if self.is_mock:
|
|
180
191
|
return ticket_payload
|
|
181
192
|
|
|
193
|
+
if self.provider != "jira":
|
|
194
|
+
raise NotImplementedError(
|
|
195
|
+
f"TMS provider '{self.provider}' is not implemented yet. Supported live provider: 'jira'."
|
|
196
|
+
)
|
|
197
|
+
|
|
182
198
|
# Live Jira Bug Creation
|
|
183
199
|
try:
|
|
184
200
|
import urllib.request
|
package/tools/universal_ir.py
CHANGED
|
@@ -271,6 +271,9 @@ class PageClassIR:
|
|
|
271
271
|
|
|
272
272
|
@dataclass
|
|
273
273
|
class TestCaseIR:
|
|
274
|
+
# IR model, not a pytest test class - prefix "Test" must not trigger collection.
|
|
275
|
+
__test__ = False
|
|
276
|
+
|
|
274
277
|
id: str
|
|
275
278
|
title: str
|
|
276
279
|
steps: List[StepIR] = field(default_factory=list)
|
package/tools/update_sentinel.py
CHANGED
|
@@ -65,8 +65,8 @@ def get_local_version():
|
|
|
65
65
|
try:
|
|
66
66
|
with open(pkg_file, "r", encoding="utf-8") as f:
|
|
67
67
|
data = json.load(f)
|
|
68
|
-
return data.get("version", "2.
|
|
69
|
-
except
|
|
68
|
+
return data.get("version", "2.3.0")
|
|
69
|
+
except (OSError, json.JSONDecodeError):
|
|
70
70
|
pass
|
|
71
71
|
|
|
72
72
|
manifest_file = os.path.join(HUB_ROOT, "skills_manifest.json")
|
|
@@ -74,11 +74,11 @@ def get_local_version():
|
|
|
74
74
|
try:
|
|
75
75
|
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
76
76
|
data = json.load(f)
|
|
77
|
-
return data.get("version", "2.
|
|
78
|
-
except
|
|
77
|
+
return data.get("version", "2.3.0")
|
|
78
|
+
except (OSError, json.JSONDecodeError):
|
|
79
79
|
pass
|
|
80
80
|
|
|
81
|
-
return "2.
|
|
81
|
+
return "2.3.0"
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def fetch_remote_version():
|
|
@@ -92,7 +92,7 @@ def fetch_remote_version():
|
|
|
92
92
|
data = json.loads(resp.read().decode("utf-8"))
|
|
93
93
|
if "version" in data:
|
|
94
94
|
return data["version"], "npm"
|
|
95
|
-
except
|
|
95
|
+
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError, KeyError):
|
|
96
96
|
pass
|
|
97
97
|
|
|
98
98
|
# 2. Secondary fallback: GitHub raw package.json
|
|
@@ -103,7 +103,7 @@ def fetch_remote_version():
|
|
|
103
103
|
data = json.loads(resp.read().decode("utf-8"))
|
|
104
104
|
if "version" in data:
|
|
105
105
|
return data["version"], "github"
|
|
106
|
-
except
|
|
106
|
+
except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError, KeyError):
|
|
107
107
|
pass
|
|
108
108
|
|
|
109
109
|
return None, None
|
|
@@ -114,7 +114,7 @@ def read_update_cache():
|
|
|
114
114
|
try:
|
|
115
115
|
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
|
116
116
|
return json.load(f)
|
|
117
|
-
except
|
|
117
|
+
except (OSError, json.JSONDecodeError):
|
|
118
118
|
pass
|
|
119
119
|
return {}
|
|
120
120
|
|
|
@@ -124,7 +124,7 @@ def write_update_cache(data):
|
|
|
124
124
|
os.makedirs(GLOBAL_DIR, exist_ok=True)
|
|
125
125
|
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
|
126
126
|
json.dump(data, f, indent=2)
|
|
127
|
-
except
|
|
127
|
+
except OSError:
|
|
128
128
|
pass
|
|
129
129
|
|
|
130
130
|
|
|
@@ -132,7 +132,16 @@ def check_for_updates(force=False, notify=True):
|
|
|
132
132
|
"""
|
|
133
133
|
Checks if a newer version exists upstream.
|
|
134
134
|
Uses a 24-hour file cache to prevent network overhead during daily workflows.
|
|
135
|
+
Air-gapped offline guard: strictly bypasses all network checks when AGENT_QA_OFFLINE is set.
|
|
135
136
|
"""
|
|
137
|
+
if (
|
|
138
|
+
os.environ.get("AGENT_QA_OFFLINE") in ("1", "true", "TRUE")
|
|
139
|
+
or os.environ.get("SYNAPSE_OFFLINE") in ("1", "true", "TRUE")
|
|
140
|
+
or os.environ.get("CI") in ("1", "true", "TRUE")
|
|
141
|
+
or os.environ.get("GITHUB_ACTIONS")
|
|
142
|
+
):
|
|
143
|
+
return False, get_local_version(), None
|
|
144
|
+
|
|
136
145
|
cache = read_update_cache()
|
|
137
146
|
now = time.time()
|
|
138
147
|
last_check = cache.get("last_check_timestamp", 0)
|
|
@@ -177,6 +186,54 @@ def print_update_banner(local_ver, remote_ver):
|
|
|
177
186
|
print(banner)
|
|
178
187
|
|
|
179
188
|
|
|
189
|
+
def _is_trusted_remote(cwd):
|
|
190
|
+
"""Verifies that git remote origin points to official Synapse Hub repository."""
|
|
191
|
+
try:
|
|
192
|
+
res = subprocess.run(
|
|
193
|
+
["git", "remote", "get-url", "origin"],
|
|
194
|
+
cwd=cwd,
|
|
195
|
+
stdout=subprocess.PIPE,
|
|
196
|
+
stderr=subprocess.PIPE,
|
|
197
|
+
text=True,
|
|
198
|
+
timeout=3.0
|
|
199
|
+
)
|
|
200
|
+
if res.returncode == 0:
|
|
201
|
+
url = res.stdout.strip().lower()
|
|
202
|
+
return "github.com/teevee998/synapse-hub" in url or "github.com/teevee998/agent-skills" in url
|
|
203
|
+
except (subprocess.SubprocessError, OSError):
|
|
204
|
+
pass
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def verify_git_provenance(cwd):
|
|
209
|
+
"""
|
|
210
|
+
Supply-chain security guard: Verifies that commits pulled into the agent environment
|
|
211
|
+
originate from trusted upstream and do not contain forged signatures.
|
|
212
|
+
"""
|
|
213
|
+
if not _is_trusted_remote(cwd):
|
|
214
|
+
return True, "Local or fork repository (trusted upstream check bypassed)"
|
|
215
|
+
try:
|
|
216
|
+
res = subprocess.run(
|
|
217
|
+
["git", "log", "-1", "--format=%H %G? %an"],
|
|
218
|
+
cwd=cwd,
|
|
219
|
+
stdout=subprocess.PIPE,
|
|
220
|
+
stderr=subprocess.PIPE,
|
|
221
|
+
text=True,
|
|
222
|
+
timeout=3.0
|
|
223
|
+
)
|
|
224
|
+
if res.returncode == 0:
|
|
225
|
+
parts = res.stdout.strip().split()
|
|
226
|
+
commit_sha = parts[0] if parts else "unknown"
|
|
227
|
+
sig_status = parts[1] if len(parts) > 1 else "N"
|
|
228
|
+
author = " ".join(parts[2:]) if len(parts) > 2 else "Unknown"
|
|
229
|
+
if sig_status == "B":
|
|
230
|
+
return False, f"Cryptographic integrity alert: Bad/forged signature on commit {commit_sha[:8]}"
|
|
231
|
+
return True, f"Verified commit {commit_sha[:8]} by {author} (sig: {sig_status})"
|
|
232
|
+
except Exception as e:
|
|
233
|
+
return False, str(e)
|
|
234
|
+
return True, "Provenance verified"
|
|
235
|
+
|
|
236
|
+
|
|
180
237
|
def perform_update():
|
|
181
238
|
"""
|
|
182
239
|
Executes 1-click update:
|
|
@@ -212,13 +269,27 @@ def perform_update():
|
|
|
212
269
|
cwd=HUB_ROOT,
|
|
213
270
|
stdout=subprocess.PIPE,
|
|
214
271
|
stderr=subprocess.PIPE,
|
|
215
|
-
text=True
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=30.0
|
|
216
274
|
)
|
|
217
275
|
if res.returncode == 0:
|
|
218
276
|
print(f" {GREEN}[OK] Git pull successful:{RESET}")
|
|
219
277
|
for line in res.stdout.strip().splitlines():
|
|
220
278
|
print(f" {line}")
|
|
279
|
+
ok, msg = verify_git_provenance(HUB_ROOT)
|
|
280
|
+
if not ok:
|
|
281
|
+
print(f" {RED}⚠️ Provenance Warning: {msg}{RESET}")
|
|
282
|
+
else:
|
|
283
|
+
print(f" {GREEN}[OK] {msg}{RESET}")
|
|
221
284
|
updated = True
|
|
285
|
+
# If this is the first pull, show the welcome landing page
|
|
286
|
+
try:
|
|
287
|
+
import welcome_page
|
|
288
|
+
if welcome_page.is_first_run(HUB_ROOT):
|
|
289
|
+
welcome_page.generate_welcome_page(target_repo=HUB_ROOT, open_browser=True)
|
|
290
|
+
welcome_page.mark_first_run_completed(HUB_ROOT)
|
|
291
|
+
except (ImportError, OSError):
|
|
292
|
+
pass
|
|
222
293
|
else:
|
|
223
294
|
print(f" ⚠️ Git pull notice: {res.stderr.strip() or res.stdout.strip()}")
|
|
224
295
|
except Exception as e:
|
|
@@ -230,12 +301,13 @@ def perform_update():
|
|
|
230
301
|
["npm", "install", "-g", "synapse-hub@latest"],
|
|
231
302
|
stdout=subprocess.PIPE,
|
|
232
303
|
stderr=subprocess.PIPE,
|
|
233
|
-
text=True
|
|
304
|
+
text=True,
|
|
305
|
+
timeout=60.0
|
|
234
306
|
)
|
|
235
307
|
if res.returncode == 0:
|
|
236
308
|
print(f" {GREEN}[OK] Global NPM package updated to latest!{RESET}")
|
|
237
309
|
updated = True
|
|
238
|
-
except
|
|
310
|
+
except (subprocess.SubprocessError, OSError):
|
|
239
311
|
pass
|
|
240
312
|
|
|
241
313
|
# 2. Check Global Plugin
|
|
@@ -244,9 +316,9 @@ def perform_update():
|
|
|
244
316
|
plugin_git = os.path.join(GLOBAL_PLUGIN_DIR, ".git")
|
|
245
317
|
if os.path.exists(plugin_git):
|
|
246
318
|
try:
|
|
247
|
-
subprocess.run(["git", "pull", "--ff-only"], cwd=GLOBAL_PLUGIN_DIR, stdout=subprocess.DEVNULL)
|
|
319
|
+
subprocess.run(["git", "pull", "--ff-only"], cwd=GLOBAL_PLUGIN_DIR, stdout=subprocess.DEVNULL, timeout=30.0)
|
|
248
320
|
print(f" {GREEN}[OK] Updated Global Plugin repository: {GLOBAL_PLUGIN_DIR}{RESET}")
|
|
249
|
-
except
|
|
321
|
+
except (subprocess.SubprocessError, OSError):
|
|
250
322
|
pass
|
|
251
323
|
else:
|
|
252
324
|
# Refresh plugin.json in global plugin
|
|
@@ -257,7 +329,7 @@ def perform_update():
|
|
|
257
329
|
if os.path.exists(src_pkg):
|
|
258
330
|
shutil.copy2(src_pkg, dst_pkg)
|
|
259
331
|
print(f" {GREEN}[OK] Refreshed plugin.json in: {GLOBAL_PLUGIN_DIR}{RESET}")
|
|
260
|
-
except
|
|
332
|
+
except OSError:
|
|
261
333
|
pass
|
|
262
334
|
|
|
263
335
|
# 3. Re-initialize SQLite Memory Cache
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Enterprise Single-Command DoD Verification Wrapper (Windows PowerShell)
|
|
2
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
3
|
+
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
|
|
4
|
+
if (-not $pythonCmd) { $pythonCmd = Get-Command py -ErrorAction SilentlyContinue }
|
|
5
|
+
if (-not $pythonCmd) { $pythonCmd = Get-Command python3 -ErrorAction SilentlyContinue }
|
|
6
|
+
|
|
7
|
+
if ($pythonCmd) {
|
|
8
|
+
& $pythonCmd.Source "$scriptDir\verify_all.py" $args
|
|
9
|
+
exit $LASTEXITCODE
|
|
10
|
+
} else {
|
|
11
|
+
Write-Error "Python 3 is required to run verify_all. Please install Python from https://python.org"
|
|
12
|
+
exit 1
|
|
13
|
+
}
|