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
|
@@ -0,0 +1,341 @@
|
|
|
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/senior_code_review.py — Senior Developer Quality & Lean Diff Reviewer
|
|
8
|
+
--------------------------------------------------------------------------
|
|
9
|
+
Reviews code changes (uncommitted, staged, or branch) like the senior engineer
|
|
10
|
+
who gets paged when it breaks:
|
|
11
|
+
1. Bugs & Edge Cases (swallowed errors, broken callers, null/edge handling)
|
|
12
|
+
2. Security & Data Risk (secrets, injection, unvalidated trust boundaries)
|
|
13
|
+
3. Scale & Concurrency (blocking calls, race conditions, memory growth)
|
|
14
|
+
4. Missing Tests (new non-trivial logic without assertions)
|
|
15
|
+
5. Performance & Hot-loops
|
|
16
|
+
6. Lean Code & Bloat (unneeded dependencies, duplicate logic, over-engineering)
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python3 tools/senior_code_review.py [target_dir]
|
|
20
|
+
python3 tools/senior_code_review.py [target_dir] --staged
|
|
21
|
+
python3 tools/senior_code_review.py [target_dir] --branch main
|
|
22
|
+
python3 tools/senior_code_review.py [target_dir] --json
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import re
|
|
28
|
+
import json
|
|
29
|
+
import subprocess
|
|
30
|
+
import argparse
|
|
31
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
34
|
+
try:
|
|
35
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
36
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Finding:
|
|
42
|
+
def __init__(self, severity: str, title: str, file_path: str, line_no: int,
|
|
43
|
+
what_is: str, problem: str, fix: str, if_skipped: str):
|
|
44
|
+
self.severity = severity # "Must fix", "Should fix", "Nice to have"
|
|
45
|
+
self.title = title
|
|
46
|
+
self.file_path = file_path
|
|
47
|
+
self.line_no = line_no
|
|
48
|
+
self.what_is = what_is
|
|
49
|
+
self.problem = problem
|
|
50
|
+
self.fix = fix
|
|
51
|
+
self.if_skipped = if_skipped
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"severity": self.severity,
|
|
56
|
+
"title": self.title,
|
|
57
|
+
"file": self.file_path,
|
|
58
|
+
"line": self.line_no,
|
|
59
|
+
"what_is": self.what_is,
|
|
60
|
+
"problem": self.problem,
|
|
61
|
+
"fix": self.fix,
|
|
62
|
+
"if_skipped": self.if_skipped
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_git_diff(repo_dir: str, target: str = "uncommitted") -> str:
|
|
67
|
+
"""Retrieves git diff based on target: uncommitted, staged, or branch/ref."""
|
|
68
|
+
cmd = ["git", "diff"]
|
|
69
|
+
if target == "staged":
|
|
70
|
+
cmd = ["git", "diff", "--cached"]
|
|
71
|
+
elif target != "uncommitted":
|
|
72
|
+
cmd = ["git", "diff", target]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
res = subprocess.run(
|
|
76
|
+
cmd,
|
|
77
|
+
cwd=repo_dir,
|
|
78
|
+
stdout=subprocess.PIPE,
|
|
79
|
+
stderr=subprocess.PIPE,
|
|
80
|
+
text=True,
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
errors="ignore",
|
|
83
|
+
check=True
|
|
84
|
+
)
|
|
85
|
+
return res.stdout
|
|
86
|
+
except Exception as e:
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_diff(diff_text: str) -> List[Dict[str, Any]]:
|
|
91
|
+
"""Parses unified diff output into file hunks and added lines."""
|
|
92
|
+
files: List[Dict[str, Any]] = []
|
|
93
|
+
current_file = None
|
|
94
|
+
current_hunk_line = 0
|
|
95
|
+
|
|
96
|
+
file_header_re = re.compile(r'^diff --git a/(.+) b/(.+)$')
|
|
97
|
+
hunk_header_re = re.compile(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@')
|
|
98
|
+
|
|
99
|
+
for line in diff_text.splitlines():
|
|
100
|
+
fm = file_header_re.match(line)
|
|
101
|
+
if fm:
|
|
102
|
+
current_file = {
|
|
103
|
+
"path": fm.group(2),
|
|
104
|
+
"added_lines": [], # (line_no, line_content)
|
|
105
|
+
"removed_lines": []
|
|
106
|
+
}
|
|
107
|
+
files.append(current_file)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if not current_file:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
hm = hunk_header_re.match(line)
|
|
114
|
+
if hm:
|
|
115
|
+
current_hunk_line = int(hm.group(1))
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
119
|
+
current_file["added_lines"].append((current_hunk_line, line[1:]))
|
|
120
|
+
current_hunk_line += 1
|
|
121
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
122
|
+
current_file["removed_lines"].append(line[1:])
|
|
123
|
+
else:
|
|
124
|
+
current_hunk_line += 1
|
|
125
|
+
|
|
126
|
+
return files
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def analyze_diff(files: List[Dict[str, Any]], repo_dir: str) -> Tuple[List[Finding], int]:
|
|
130
|
+
"""Inspects diff files against senior dev quality & lean heuristics."""
|
|
131
|
+
findings: List[Finding] = []
|
|
132
|
+
lines_cuttable = 0
|
|
133
|
+
|
|
134
|
+
has_new_test = False
|
|
135
|
+
has_new_business_code = False
|
|
136
|
+
|
|
137
|
+
# Check overall changes for test balance
|
|
138
|
+
for f in files:
|
|
139
|
+
path = f["path"]
|
|
140
|
+
if "test" in path.lower() or "spec" in path.lower():
|
|
141
|
+
if f["added_lines"]:
|
|
142
|
+
has_new_test = True
|
|
143
|
+
else:
|
|
144
|
+
ext = os.path.splitext(path)[1].lower()
|
|
145
|
+
if ext in {".py", ".ts", ".js", ".java", ".groovy", ".go", ".rs"}:
|
|
146
|
+
if len(f["added_lines"]) > 5:
|
|
147
|
+
has_new_business_code = True
|
|
148
|
+
|
|
149
|
+
# Rule checks on added lines
|
|
150
|
+
secret_pattern = re.compile(r'(?:api[_-]?key|password|secret|bearer|token)\s*=\s*[\'"][A-Za-z0-9_\-\.]{8,}[\'"]', re.I)
|
|
151
|
+
swallowed_catch_py = re.compile(r'except\s*(?:Exception)?\s*:\s*(?:pass|\.\.\.)')
|
|
152
|
+
swallowed_catch_js = re.compile(r'catch\s*\([^)]*\)\s*\{\s*\}')
|
|
153
|
+
static_sleep_pattern = re.compile(r'(?:Thread\.sleep|time\.sleep|sleep\(\d{3,}\))')
|
|
154
|
+
raw_xpath_pattern = re.compile(r'findTestObject|\.locator\(["\']//|\.locator\(["\']xpath=')
|
|
155
|
+
|
|
156
|
+
for f in files:
|
|
157
|
+
path = f["path"]
|
|
158
|
+
for line_no, content in f["added_lines"]:
|
|
159
|
+
stripped = content.strip()
|
|
160
|
+
|
|
161
|
+
# 1. Security: Hardcoded Credentials
|
|
162
|
+
if secret_pattern.search(stripped) and "test" not in path.lower():
|
|
163
|
+
findings.append(Finding(
|
|
164
|
+
severity="Must fix",
|
|
165
|
+
title="Potential hardcoded secret or token detected",
|
|
166
|
+
file_path=path,
|
|
167
|
+
line_no=line_no,
|
|
168
|
+
what_is="A secret or token literal assignment in source code.",
|
|
169
|
+
problem="Exposing credentials in source control allows unauthorized access and fails security scans.",
|
|
170
|
+
fix="Extract the secret to an environment variable or encrypted profile config.",
|
|
171
|
+
if_skipped="Credentials risk leaking to version control and security audits will fail."
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
# 2. Correctness: Swallowed Exceptions
|
|
175
|
+
if swallowed_catch_py.search(stripped) or swallowed_catch_js.search(stripped):
|
|
176
|
+
findings.append(Finding(
|
|
177
|
+
severity="Must fix",
|
|
178
|
+
title="Swallowed exception / empty catch block",
|
|
179
|
+
file_path=path,
|
|
180
|
+
line_no=line_no,
|
|
181
|
+
what_is="An exception catch block that suppresses errors without logging or handling.",
|
|
182
|
+
problem="Silent failures hide root causes, corrupt downstream state, and lead to mysterious bugs in production.",
|
|
183
|
+
fix="Log the exception with actionable context or rethrow a domain-specific error.",
|
|
184
|
+
if_skipped="Failures will be completely invisible and impossible to debug under load."
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
# 3. Flakiness / Concurrency: Static Sleep
|
|
188
|
+
if static_sleep_pattern.search(stripped):
|
|
189
|
+
findings.append(Finding(
|
|
190
|
+
severity="Should fix",
|
|
191
|
+
title="Static sleep detected (pipeline flakiness risk)",
|
|
192
|
+
file_path=path,
|
|
193
|
+
line_no=line_no,
|
|
194
|
+
what_is="A static Thread.sleep or time.sleep delay in execution flow.",
|
|
195
|
+
problem="Static delays waste CI time under fast networks and fail randomly under heavy load.",
|
|
196
|
+
fix="Replace with a dynamic wait: element visibility check, network idle state, or condition polling.",
|
|
197
|
+
if_skipped="Test runs will suffer non-deterministic flakiness and slower CI turnaround."
|
|
198
|
+
))
|
|
199
|
+
|
|
200
|
+
# 4. Clean POM Architecture: Raw locators in scripts
|
|
201
|
+
if ("script" in path.lower() or "tests" in path.lower()) and raw_xpath_pattern.search(stripped):
|
|
202
|
+
findings.append(Finding(
|
|
203
|
+
severity="Should fix",
|
|
204
|
+
title="Raw selector or object reference inside test script",
|
|
205
|
+
file_path=path,
|
|
206
|
+
line_no=line_no,
|
|
207
|
+
what_is="A raw XPath or direct object call inside a high-level test script.",
|
|
208
|
+
problem="Violates Clean POM / Flat Architecture separation of concerns; UI changes break scripts directly.",
|
|
209
|
+
fix="Encapsulate this locator and interaction inside a Page Class extending BasePage.",
|
|
210
|
+
if_skipped="Any DOM markup change requires updating multiple scripts instead of a single Page Class."
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
# 5. Lean: Commented-out dead code blocks
|
|
214
|
+
if (stripped.startswith("//") or stripped.startswith("#")) and len(stripped) > 20:
|
|
215
|
+
if re.match(r'^(?://|#)\s*(?:const|let|var|def|function|class|import|return)\b', stripped):
|
|
216
|
+
lines_cuttable += 1
|
|
217
|
+
findings.append(Finding(
|
|
218
|
+
severity="Nice to have",
|
|
219
|
+
title="Commented-out dead code block",
|
|
220
|
+
file_path=path,
|
|
221
|
+
line_no=line_no,
|
|
222
|
+
what_is="A commented-out code statement left in the diff.",
|
|
223
|
+
problem="Dead code creates visual clutter, confuses future maintainers, and rots over time.",
|
|
224
|
+
fix="Delete the commented code. Git history preserves past implementations.",
|
|
225
|
+
if_skipped="Technical debt and confusion will accumulate across commits."
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
# 6. Completeness: Non-trivial logic without any test
|
|
229
|
+
if has_new_business_code and not has_new_test:
|
|
230
|
+
findings.append(Finding(
|
|
231
|
+
severity="Should fix",
|
|
232
|
+
title="Non-trivial business code added without accompanying tests",
|
|
233
|
+
file_path="[Multiple files]",
|
|
234
|
+
line_no=1,
|
|
235
|
+
what_is="Substantial new implementation code with zero test additions or updates.",
|
|
236
|
+
problem="Risky logic ships unverified; regressions will not be caught automatically by CI.",
|
|
237
|
+
fix="Add at least one targeted unit or integration test proving this logic works on edge cases.",
|
|
238
|
+
if_skipped="Future refactoring risks breaking this behavior silently."
|
|
239
|
+
))
|
|
240
|
+
|
|
241
|
+
return findings, lines_cuttable
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def format_report(findings: List[Finding], files: List[Dict[str, Any]], lines_cuttable: int) -> str:
|
|
245
|
+
"""Renders the senior code review report in plain, pragmatic markdown."""
|
|
246
|
+
must_fix = [f for f in findings if f.severity == "Must fix"]
|
|
247
|
+
should_fix = [f for f in findings if f.severity == "Should fix"]
|
|
248
|
+
nice_to_have = [f for f in findings if f.severity == "Nice to have"]
|
|
249
|
+
|
|
250
|
+
lines = [
|
|
251
|
+
"# Senior Developer Quality & Lean Code Review",
|
|
252
|
+
"",
|
|
253
|
+
"> **Perspective:** Reviewed like the senior engineer who will be paged when this breaks.",
|
|
254
|
+
"",
|
|
255
|
+
f"- **Files Changed:** `{len(files)}`",
|
|
256
|
+
f"- **Findings:** `{len(findings)}` (`{len(must_fix)}` Must fix, `{len(should_fix)}` Should fix, `{len(nice_to_have)}` Nice to have)",
|
|
257
|
+
"",
|
|
258
|
+
"---",
|
|
259
|
+
"",
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
if not files:
|
|
263
|
+
lines.append("ℹ️ **No changes found in the specified target.** Working directory is clean.")
|
|
264
|
+
return "\n".join(lines)
|
|
265
|
+
|
|
266
|
+
if not findings:
|
|
267
|
+
lines.append("🎉 **Verdict: Looks good. Clean, lean, and ready to ship.**")
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
|
|
270
|
+
# Grouped findings
|
|
271
|
+
count = 1
|
|
272
|
+
for group_name, group_items in [("🚨 Must Fix (Blockers)", must_fix),
|
|
273
|
+
("⚠️ Should Fix (Reliability & Scale)", should_fix),
|
|
274
|
+
("💡 Nice to Have (Lean & Cleanup)", nice_to_have)]:
|
|
275
|
+
if not group_items:
|
|
276
|
+
continue
|
|
277
|
+
lines.append(f"### {group_name}")
|
|
278
|
+
lines.append("")
|
|
279
|
+
for item in group_items:
|
|
280
|
+
loc = f"`{item.file_path}:{item.line_no}`" if item.line_no else f"`{item.file_path}`"
|
|
281
|
+
lines.append(f"#### {count}. **{item.title}** ({loc})")
|
|
282
|
+
lines.append(f"- **What this is:** {item.what_is}")
|
|
283
|
+
lines.append(f"- **Problem:** {item.problem}")
|
|
284
|
+
lines.append(f"- **Fix:** {item.fix}")
|
|
285
|
+
lines.append(f"- **If we skip it:** {item.if_skipped}")
|
|
286
|
+
lines.append("")
|
|
287
|
+
count += 1
|
|
288
|
+
|
|
289
|
+
lines.append("---")
|
|
290
|
+
lines.append("### Verdict")
|
|
291
|
+
if must_fix:
|
|
292
|
+
lines.append(f"❌ **Action Required:** Resolve `{len(must_fix)}` blocker(s) before merging.")
|
|
293
|
+
elif should_fix:
|
|
294
|
+
lines.append(f"⚠️ **Caution:** Address `{len(should_fix)}` recommendation(s) to avoid flaky runs or debt.")
|
|
295
|
+
else:
|
|
296
|
+
lines.append("✅ **Approved with minor notes:** Safe to proceed.")
|
|
297
|
+
|
|
298
|
+
if lines_cuttable > 0:
|
|
299
|
+
lines.append(f"\n📉 **Lean Opportunity:** ~`{lines_cuttable}` unnecessary line(s) can be safely deleted.")
|
|
300
|
+
|
|
301
|
+
return "\n".join(lines)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def run_review(repo_dir: str, target: str = "uncommitted", as_json: bool = False) -> int:
|
|
305
|
+
"""Runs the diff extraction and review."""
|
|
306
|
+
repo_dir = os.path.abspath(repo_dir)
|
|
307
|
+
diff_text = get_git_diff(repo_dir, target)
|
|
308
|
+
files = parse_diff(diff_text)
|
|
309
|
+
findings, lines_cuttable = analyze_diff(files, repo_dir)
|
|
310
|
+
|
|
311
|
+
if as_json:
|
|
312
|
+
payload = {
|
|
313
|
+
"target": target,
|
|
314
|
+
"files_count": len(files),
|
|
315
|
+
"findings_count": len(findings),
|
|
316
|
+
"lines_cuttable": lines_cuttable,
|
|
317
|
+
"findings": [f.to_dict() for f in findings]
|
|
318
|
+
}
|
|
319
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
320
|
+
return 0 if not any(f.severity == "Must fix" for f in findings) else 1
|
|
321
|
+
|
|
322
|
+
print("\n" + format_report(findings, files, lines_cuttable))
|
|
323
|
+
return 0 if not any(f.severity == "Must fix" for f in findings) else 1
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main():
|
|
327
|
+
parser = argparse.ArgumentParser(
|
|
328
|
+
description="Senior Developer Quality & Lean Diff Reviewer (bugs, security, scale, tests, bloat)"
|
|
329
|
+
)
|
|
330
|
+
parser.add_argument("target_dir", nargs="?", default=".", help="Repository directory (default: current directory)")
|
|
331
|
+
parser.add_argument("--staged", action="store_true", help="Review staged git changes")
|
|
332
|
+
parser.add_argument("--branch", default=None, help="Review changes against a git branch or commit ref")
|
|
333
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
334
|
+
|
|
335
|
+
args = parser.parse_args()
|
|
336
|
+
diff_target = "staged" if args.staged else (args.branch if args.branch else "uncommitted")
|
|
337
|
+
sys.exit(run_review(args.target_dir, diff_target, args.json))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
main()
|
package/tools/setup_project.py
CHANGED
|
@@ -219,7 +219,7 @@ def install_git_pre_push_hook(target_dir):
|
|
|
219
219
|
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
220
220
|
pkg_ver = json.load(f).get("version", pkg_ver)
|
|
221
221
|
except (IOError, json.JSONDecodeError, KeyError):
|
|
222
|
-
|
|
222
|
+
pkg_ver = "2.4.3"
|
|
223
223
|
|
|
224
224
|
verify_all_script = os.path.join(SCRIPT_DIR, "verify_all.py").replace("\\", "/")
|
|
225
225
|
analyze_script = os.path.join(SCRIPT_DIR, "analyze_project.py").replace("\\", "/")
|
|
@@ -281,7 +281,7 @@ echo "[AI-GUARD - READY] Pushing to remote...\\n"
|
|
|
281
281
|
|
|
282
282
|
# Attempt to make executable on Unix-like environments
|
|
283
283
|
try:
|
|
284
|
-
os.chmod(hook_file, 0o755)
|
|
284
|
+
os.chmod(hook_file, 0o755) # nosec B103
|
|
285
285
|
except OSError:
|
|
286
286
|
pass
|
|
287
287
|
|
|
@@ -311,8 +311,8 @@ def ensure_gitignore_protection(target_dir):
|
|
|
311
311
|
f.write(f"{e}\n")
|
|
312
312
|
print(f" {GREEN}[OK] Protected local cache in .gitignore:{RESET} {gitignore_path}")
|
|
313
313
|
return True
|
|
314
|
-
except OSError:
|
|
315
|
-
|
|
314
|
+
except OSError as err:
|
|
315
|
+
sys.stderr.write(f"Notice: .gitignore update skipped: {err}\n")
|
|
316
316
|
return False
|
|
317
317
|
|
|
318
318
|
|
|
@@ -358,7 +358,7 @@ fi
|
|
|
358
358
|
f.write(final_content)
|
|
359
359
|
|
|
360
360
|
try:
|
|
361
|
-
os.chmod(hook_file, 0o755)
|
|
361
|
+
os.chmod(hook_file, 0o755) # nosec B103
|
|
362
362
|
except OSError:
|
|
363
363
|
pass
|
|
364
364
|
|
|
@@ -372,7 +372,7 @@ def inject_antigravity_global_plugin():
|
|
|
372
372
|
"""
|
|
373
373
|
Automatically injects Synapse-Hub into Antigravity's Global Plugin Directory
|
|
374
374
|
(~/.gemini/config/plugins/synapse-hub).
|
|
375
|
-
Ensures that running `npx synapse-hub setup` activates the
|
|
375
|
+
Ensures that running `npx synapse-hub setup` activates the 42 skills and 6 subagents
|
|
376
376
|
globally across ALL projects in Google Antigravity.
|
|
377
377
|
"""
|
|
378
378
|
try:
|
|
@@ -412,7 +412,7 @@ def inject_antigravity_global_plugin():
|
|
|
412
412
|
try:
|
|
413
413
|
os.unlink(dst)
|
|
414
414
|
except OSError:
|
|
415
|
-
|
|
415
|
+
unlinked = False
|
|
416
416
|
if sys.platform == "win32":
|
|
417
417
|
try:
|
|
418
418
|
import _winapi
|
|
@@ -420,7 +420,7 @@ def inject_antigravity_global_plugin():
|
|
|
420
420
|
_winapi.CreateJunction(src, dst)
|
|
421
421
|
return
|
|
422
422
|
except (OSError, ImportError, AttributeError):
|
|
423
|
-
|
|
423
|
+
junction_created = False
|
|
424
424
|
shutil.copytree(src, dst, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.tmp"))
|
|
425
425
|
|
|
426
426
|
sync_dir(os.path.join(HUB_ROOT, ".agents", "skills"), os.path.join(target_plugin_dir, "skills"))
|
|
@@ -679,7 +679,7 @@ def run_setup(target_repo, dry_run=False, profile="auto", no_global=True, no_git
|
|
|
679
679
|
)
|
|
680
680
|
welcome_url = f"file://{welcome_file}"
|
|
681
681
|
except (ImportError, OSError, ValueError):
|
|
682
|
-
|
|
682
|
+
welcome_url = ""
|
|
683
683
|
|
|
684
684
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
685
685
|
print(f"{BOLD}{CYAN} 🎉 PROJECT IS NOW 100% AI-READY (LEVEL 5 SETUP) {RESET}")
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
Tier-2 Skill Routing & Collision Guard (Addy Osmani Pattern)
|
|
8
|
+
------------------------------------------------------------
|
|
9
|
+
Computes pairwise cosine similarity across all SKILL.md descriptions.
|
|
10
|
+
Operates 100% offline with zero LLM tokens using a lightweight suffix-stripping
|
|
11
|
+
stemmer heuristic and TF-IDF word frequency representation.
|
|
12
|
+
|
|
13
|
+
Enforces:
|
|
14
|
+
- Warning when description similarity >= 0.50 (semantic proximity)
|
|
15
|
+
- Critical Error (Quality Gate Failure) when similarity >= 0.75 (ambiguous routing)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python tools/skill_collision_guard.py [--dir .agents/skills] [--threshold 0.75] [--json]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import re
|
|
24
|
+
import math
|
|
25
|
+
import json
|
|
26
|
+
import argparse
|
|
27
|
+
from typing import Dict, List, Tuple
|
|
28
|
+
from collections import Counter
|
|
29
|
+
|
|
30
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
31
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
34
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
STOPWORDS = {
|
|
39
|
+
"a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are",
|
|
40
|
+
"aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both",
|
|
41
|
+
"but", "by", "can", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't",
|
|
42
|
+
"doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't",
|
|
43
|
+
"has", "hasn't", "have", "haven't", "having", "he", "her", "here", "hers", "herself", "him",
|
|
44
|
+
"himself", "his", "how", "i", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself",
|
|
45
|
+
"let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on",
|
|
46
|
+
"once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same",
|
|
47
|
+
"shan't", "she", "should", "shouldn't", "so", "some", "such", "than", "that", "the", "their",
|
|
48
|
+
"theirs", "them", "themselves", "then", "there", "these", "they", "this", "those", "through",
|
|
49
|
+
"to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "were", "weren't", "what",
|
|
50
|
+
"when", "where", "which", "while", "who", "whom", "why", "with", "won't", "would", "wouldn't",
|
|
51
|
+
"you", "your", "yours", "yourself", "yourselves", "use", "when", "allows", "provides", "designed"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def strip_suffix(word: str) -> str:
|
|
56
|
+
"""Lightweight suffix-stripping heuristic (Addy Osmani Tier-2 standard)."""
|
|
57
|
+
if len(word) <= 4:
|
|
58
|
+
return word
|
|
59
|
+
for suffix in ("ing", "tion", "ment", "ness", "able", "ible", "ize", "ise", "ies", "es", "ed", "ly", "al", "s"):
|
|
60
|
+
if word.endswith(suffix) and len(word) - len(suffix) >= 3:
|
|
61
|
+
return word[:-len(suffix)]
|
|
62
|
+
return word
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def tokenize(text: str) -> List[str]:
|
|
66
|
+
"""Tokenizes text, strips punctuation, removes stopwords and applies suffix stripping."""
|
|
67
|
+
words = re.findall(r"\b[a-zA-Z0-9_\-]+\b", text.lower())
|
|
68
|
+
tokens = []
|
|
69
|
+
for w in words:
|
|
70
|
+
cleaned = re.sub(r"^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$", "", w)
|
|
71
|
+
if len(cleaned) >= 2 and cleaned not in STOPWORDS:
|
|
72
|
+
tokens.append(strip_suffix(cleaned))
|
|
73
|
+
return tokens
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_tf_vector(tokens: List[str]) -> Dict[str, float]:
|
|
77
|
+
"""Builds normalized term-frequency vector."""
|
|
78
|
+
counts = Counter(tokens)
|
|
79
|
+
total = len(tokens)
|
|
80
|
+
if total == 0:
|
|
81
|
+
return {}
|
|
82
|
+
return {word: count / total for word, count in counts.items()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cosine_similarity(vec1: Dict[str, float], vec2: Dict[str, float]) -> float:
|
|
86
|
+
"""Computes cosine similarity between two term-frequency vectors."""
|
|
87
|
+
intersection = set(vec1.keys()) & set(vec2.keys())
|
|
88
|
+
numerator = sum(vec1[x] * vec2[x] for x in intersection)
|
|
89
|
+
sum1 = sum(v ** 2 for v in vec1.values())
|
|
90
|
+
sum2 = sum(v ** 2 for v in vec2.values())
|
|
91
|
+
denominator = math.sqrt(sum1) * math.sqrt(sum2)
|
|
92
|
+
if not denominator:
|
|
93
|
+
return 0.0
|
|
94
|
+
return float(numerator) / denominator
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SkillCollisionGuard:
|
|
98
|
+
"""Audits skill descriptions across repository for routing collisions and ambiguous triggers."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, skills_dir: str = ".agents/skills", threshold: float = 0.75, warn_threshold: float = 0.50):
|
|
101
|
+
self.skills_dir = skills_dir
|
|
102
|
+
self.threshold = threshold
|
|
103
|
+
self.warn_threshold = warn_threshold
|
|
104
|
+
|
|
105
|
+
def load_descriptions(self) -> Dict[str, str]:
|
|
106
|
+
"""Loads all SKILL.md files and extracts their description strings."""
|
|
107
|
+
descriptions = {}
|
|
108
|
+
if not os.path.exists(self.skills_dir):
|
|
109
|
+
return descriptions
|
|
110
|
+
|
|
111
|
+
for root, _, files in os.walk(self.skills_dir):
|
|
112
|
+
for file in files:
|
|
113
|
+
if file.lower() == "skill.md":
|
|
114
|
+
full_path = os.path.join(root, file)
|
|
115
|
+
skill_name, desc = self._extract_frontmatter(full_path)
|
|
116
|
+
if skill_name and desc:
|
|
117
|
+
descriptions[skill_name] = desc
|
|
118
|
+
return descriptions
|
|
119
|
+
|
|
120
|
+
def _extract_frontmatter(self, path: str) -> Tuple[str, str]:
|
|
121
|
+
try:
|
|
122
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
123
|
+
content = f.read()
|
|
124
|
+
except OSError:
|
|
125
|
+
return "", ""
|
|
126
|
+
|
|
127
|
+
match = re.search(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
|
|
128
|
+
if match:
|
|
129
|
+
fm = match.group(1)
|
|
130
|
+
name_m = re.search(r"^name:\s*(.+)$", fm, re.MULTILINE)
|
|
131
|
+
desc_m = re.search(r"^description:\s*[\"']?(.*?)[\"']?$", fm, re.MULTILINE)
|
|
132
|
+
name = name_m.group(1).strip() if name_m else os.path.basename(os.path.dirname(path))
|
|
133
|
+
desc = desc_m.group(1).strip() if desc_m else ""
|
|
134
|
+
return name, desc
|
|
135
|
+
|
|
136
|
+
fallback_name = os.path.basename(os.path.dirname(path))
|
|
137
|
+
return fallback_name, ""
|
|
138
|
+
|
|
139
|
+
def audit(self) -> Tuple[bool, List[Dict[str, any]]]:
|
|
140
|
+
"""
|
|
141
|
+
Computes pairwise similarity.
|
|
142
|
+
Returns (is_clean, report_items).
|
|
143
|
+
"""
|
|
144
|
+
descriptions = self.load_descriptions()
|
|
145
|
+
names = sorted(list(descriptions.keys()))
|
|
146
|
+
vectors = {name: build_tf_vector(tokenize(descriptions[name])) for name in names}
|
|
147
|
+
|
|
148
|
+
findings = []
|
|
149
|
+
has_critical = False
|
|
150
|
+
|
|
151
|
+
for i in range(len(names)):
|
|
152
|
+
for j in range(i + 1, len(names)):
|
|
153
|
+
name_a = names[i]
|
|
154
|
+
name_b = names[j]
|
|
155
|
+
sim = cosine_similarity(vectors[name_a], vectors[name_b])
|
|
156
|
+
|
|
157
|
+
if sim >= self.threshold:
|
|
158
|
+
has_critical = True
|
|
159
|
+
findings.append({
|
|
160
|
+
"level": "CRITICAL",
|
|
161
|
+
"skill_a": name_a,
|
|
162
|
+
"skill_b": name_b,
|
|
163
|
+
"similarity": round(sim, 3),
|
|
164
|
+
"message": f"Critical Collision: '{name_a}' and '{name_b}' have cosine similarity {sim:.3f} >= {self.threshold}"
|
|
165
|
+
})
|
|
166
|
+
elif sim >= self.warn_threshold:
|
|
167
|
+
findings.append({
|
|
168
|
+
"level": "WARNING",
|
|
169
|
+
"skill_a": name_a,
|
|
170
|
+
"skill_b": name_b,
|
|
171
|
+
"similarity": round(sim, 3),
|
|
172
|
+
"message": f"Proximity Warning: '{name_a}' and '{name_b}' have similarity {sim:.3f} >= {self.warn_threshold}"
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return (not has_critical), findings
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_guard(skills_dir: str = ".agents/skills", threshold: float = 0.75, warn_threshold: float = 0.50, output_json: bool = False) -> int:
|
|
179
|
+
guard = SkillCollisionGuard(skills_dir, threshold=threshold, warn_threshold=warn_threshold)
|
|
180
|
+
passed, findings = guard.audit()
|
|
181
|
+
|
|
182
|
+
if output_json:
|
|
183
|
+
print(json.dumps({
|
|
184
|
+
"skills_dir": skills_dir,
|
|
185
|
+
"passed": passed,
|
|
186
|
+
"threshold": threshold,
|
|
187
|
+
"warn_threshold": warn_threshold,
|
|
188
|
+
"findings": findings
|
|
189
|
+
}, indent=2))
|
|
190
|
+
return 0 if passed else 1
|
|
191
|
+
|
|
192
|
+
print("\n" + "=" * 70)
|
|
193
|
+
print(" TIER-2 SKILL ROUTING & COLLISION GUARD (ADDY OSMANI PATTERN)")
|
|
194
|
+
print("=" * 70)
|
|
195
|
+
print(f"Skills Directory : {skills_dir}")
|
|
196
|
+
print(f"Collision Threshold: {threshold} (FAIL) | Warning Threshold: {warn_threshold} (WARN)")
|
|
197
|
+
|
|
198
|
+
criticals = [f for f in findings if f["level"] == "CRITICAL"]
|
|
199
|
+
warnings = [f for f in findings if f["level"] == "WARNING"]
|
|
200
|
+
|
|
201
|
+
if warnings:
|
|
202
|
+
print(f"\n⚠️ {len(warnings)} Near-Neighbor Warnings detected (similarity >= {warn_threshold}):")
|
|
203
|
+
for w in warnings:
|
|
204
|
+
print(f" - {w['message']}")
|
|
205
|
+
|
|
206
|
+
if criticals:
|
|
207
|
+
print(f"\n❌ {len(criticals)} CRITICAL COLLISION(S) detected (similarity >= {threshold}):")
|
|
208
|
+
for c in criticals:
|
|
209
|
+
print(f" [FAIL] {c['message']}")
|
|
210
|
+
print("\n[DoD BLOCKED] High similarity causes routing ambiguity and context contamination.")
|
|
211
|
+
print("=" * 70 + "\n")
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
print(f"\n✅ [PASS] 0 routing collisions detected. All skill descriptions uniquely segmented.\n")
|
|
215
|
+
print("=" * 70 + "\n")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
parser = argparse.ArgumentParser(description="Tier-2 Skill Routing & Collision Guard")
|
|
221
|
+
parser.add_argument("--dir", default=".agents/skills", help="Directory containing skill packages")
|
|
222
|
+
parser.add_argument("--threshold", type=float, default=0.75, help="Critical similarity threshold (default: 0.75)")
|
|
223
|
+
parser.add_argument("--warn-threshold", type=float, default=0.50, help="Warning similarity threshold (default: 0.50)")
|
|
224
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
225
|
+
args = parser.parse_args()
|
|
226
|
+
|
|
227
|
+
return run_guard(skills_dir=args.dir, threshold=args.threshold, warn_threshold=args.warn_threshold, output_json=args.json)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
sys.exit(main())
|