synapse-hub 2.7.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/live-explorer.md +2 -0
- package/.agents/agents/pom-crafter.md +2 -0
- package/.agents/agents/test-runner.md +3 -0
- package/.agents/rules/communication-style.md +4 -0
- package/.agents/rules/context-engineering.md +1 -0
- package/.agents/rules/git-push-protocol.md +17 -0
- package/.agents/skills/agentless-qa-repair/examples/three_phase_repair_workflow.md +40 -0
- package/.agents/skills/antigravity-test-sentinel/examples/sprint_dod_enforcement.md +47 -0
- package/.agents/skills/auth-session-and-cookie-manager/examples/multi_role_auth_matrix.md +70 -0
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_playwright_pom.ts +52 -0
- package/.agents/skills/failure-autopsy-and-healing/examples/closed_loop_healing_walkthrough.md +48 -0
- package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
- package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/examples/dspy_assertion_optimizer_case.md +46 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +1 -1
- package/.agents/skills/qa-aci-interface/examples/surgical_code_window_walkthrough.md +40 -0
- package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
- package/.agents/skills/review-spec/SKILL.md +1 -1
- package/.agents/skills/review-spec/examples/pocock_grilling_decision_rubric.md +40 -0
- package/.agents/skills/senior-code-review/SKILL.md +1 -1
- package/.agents/skills/skillspector-security-guard/examples/audit_threat_vectors_report.json +35 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +1 -1
- package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
- package/.agents/skills/tot-exploratory-testing/examples/payment_gateway_tot_tree.json +62 -0
- package/.agents/skills/verification-before-completion/SKILL.md +2 -2
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/examples/enterprise_tasks_suite.json +38 -0
- package/.agents/skills/web-vitals-and-performance-profiler/examples/playwright_cwv_profiler_fixture.ts +61 -0
- package/AGENTS.md +6 -4
- package/CLAUDE.md +2 -1
- package/README.en.md +71 -292
- package/README.md +69 -304
- package/action.yml +1 -1
- package/bin/agent-qa.js +2 -2
- package/docs/ARCHITECTURE.en.md +192 -0
- package/docs/ARCHITECTURE.md +135 -88
- package/docs/CI_CD_INTEGRATION.en.md +96 -0
- package/docs/CI_CD_INTEGRATION.md +24 -22
- package/docs/CLI_REFERENCE.en.md +174 -0
- package/docs/CLI_REFERENCE.md +120 -115
- package/docs/CONTRIBUTING.en.md +49 -0
- package/docs/CONTRIBUTING.md +26 -68
- package/docs/QUALITY_GATES.en.md +115 -0
- package/docs/QUALITY_GATES.md +115 -0
- package/docs/research/agent_skill_training_analysis.md +1 -1
- package/package.json +1 -1
- package/scripts/package_dist.py +2 -1
- package/skills_manifest.json +6 -6
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +2 -1
- package/templates/ai_bridges/copilot-instructions.md +2 -2
- package/tools/__init__.py +18 -1
- package/tools/_openapi_parser.py +1 -1
- package/tools/agent_canary.py +42 -30
- package/tools/agentless_patcher.py +2 -2
- package/tools/analyze_project.py +3 -5
- package/tools/api_drift_sentinel.py +4 -4
- package/tools/api_fuzzer.py +8 -8
- package/tools/auto_heal.py +4 -5
- package/tools/auto_heal_reflexion.py +10 -2
- package/tools/autogen_qa_broker.py +2 -2
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +2 -3
- package/tools/bug_reporter.py +11 -13
- package/tools/bump_version.py +90 -19
- package/tools/cache_resolver.py +78 -83
- package/tools/check_env.py +9 -1
- package/tools/check_version_sync.py +58 -4
- package/tools/ci_healing_hook.py +4 -6
- package/tools/cli.py +5 -7
- package/tools/config_decoupler.py +2 -3
- package/tools/contract_validator.py +141 -0
- package/tools/core/__init__.py +52 -0
- package/tools/count_tokens.py +22 -20
- package/tools/dashboard_generator.py +629 -1000
- package/tools/db_sandbox.py +87 -62
- package/tools/debt_harvester.py +3 -3
- package/tools/distill_log.py +3 -3
- package/tools/dspy_qa_signatures.py +3 -4
- package/tools/export_postman_collection.py +8 -8
- package/tools/flaky_detector.py +2 -2
- package/tools/fuzz_payloads.py +5 -5
- package/tools/generate_allure_report.py +1 -1
- package/tools/generate_jmx_plan.py +4 -4
- package/tools/gherkin_suite.py +1 -1
- package/tools/git_pre_commit_guard.py +2 -2
- package/tools/goldfish_review.py +2 -2
- package/tools/heal/__init__.py +28 -0
- package/tools/heal_pr_automator.py +6 -6
- package/tools/heuristic_locator_matcher.py +2 -2
- package/tools/infer_linter.py +143 -29
- package/tools/interview_story.py +0 -1
- package/tools/jtl_analyzer.py +11 -11
- package/tools/lint/__init__.py +30 -0
- package/tools/lint_ai_docs.py +42 -0
- package/tools/lookup_tc.py +1 -1
- package/tools/matrix_sharder.py +15 -13
- package/tools/mcp/__init__.py +16 -0
- package/tools/mcp_excel_server.py +2 -2
- package/tools/mcp_server.py +9 -7
- package/tools/md_to_xlsx.py +26 -26
- package/tools/micro_mutator.py +32 -15
- package/tools/network_mock_shield.py +27 -27
- package/tools/newman_analyzer.py +2 -3
- package/tools/omni_emitter.py +5 -8
- package/tools/omni_parser.py +5 -5
- package/tools/omni_transpiler.py +5 -5
- package/tools/output_formatter.py +19 -0
- package/tools/perf/__init__.py +24 -0
- package/tools/pinpoint_failure.py +1 -1
- package/tools/project_state.py +0 -2
- package/tools/qa_event_stream.py +1 -1
- package/tools/repo_map.py +8 -8
- package/tools/report/__init__.py +32 -0
- package/tools/reversible_sandbox.py +3 -4
- package/tools/safety_firewall.py +2 -2
- package/tools/senior_code_review.py +4 -3
- package/tools/setup_project.py +10 -12
- package/tools/skill_collision_guard.py +3 -3
- package/tools/skill_vault.py +94 -6
- package/tools/skillspector.py +10 -8
- package/tools/smart_visual_masker.py +4 -6
- package/tools/spec_drift_sentinel.py +1 -2
- package/tools/swe_qa_aci.py +2 -2
- package/tools/synapse_qa_bench.py +3 -3
- package/tools/sync_agent_rules.py +18 -3
- package/tools/telemetry_correlator.py +3 -2
- package/tools/test_impact.py +2 -1
- package/tools/test_memory_os.py +15 -5
- package/tools/textgrad_mutation_optimizer.py +3 -3
- package/tools/tms_sync_bridge.py +5 -5
- package/tools/tot_exploratory_tester.py +5 -6
- package/tools/traffic_session_converter.py +9 -9
- package/tools/universal_ir.py +2 -3
- package/tools/update_sentinel.py +6 -9
- package/tools/verify_all.py +12 -2
- package/tools/visual_diff_canvas.py +4 -4
- package/tools/visual_som_helper.py +15 -13
- package/tools/watch_mode.py +1 -1
- package/tools/wcag_audit.py +22 -1
- package/tools/welcome_page.py +5 -6
package/tools/api_fuzzer.py
CHANGED
|
@@ -24,7 +24,7 @@ import urllib.request
|
|
|
24
24
|
import urllib.error
|
|
25
25
|
import urllib.parse
|
|
26
26
|
import argparse
|
|
27
|
-
from typing import Dict, Any, List, Optional
|
|
27
|
+
from typing import Dict, Any, List, Optional
|
|
28
28
|
|
|
29
29
|
# Enforce UTF-8 stdout encoding for Windows/macOS/Linux compatibility
|
|
30
30
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -578,13 +578,13 @@ class ApiFuzzer:
|
|
|
578
578
|
print(f" Verdict : {GREEN}[PASS] API resilient against input fuzzing & chaos.{RESET}")
|
|
579
579
|
else:
|
|
580
580
|
print(f" Verdict : {RED}[FAIL] {total_failures} defect alarms detected. Backend fixes required!{RESET}")
|
|
581
|
-
print(
|
|
581
|
+
print("======================================================================\n")
|
|
582
582
|
|
|
583
583
|
# Save Report
|
|
584
584
|
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
585
585
|
report_path = os.path.join(REPORTS_DIR, "api_fuzz_report.json")
|
|
586
|
-
with open(report_path, "w", encoding="utf-8") as
|
|
587
|
-
json.dump(scorecard,
|
|
586
|
+
with open(report_path, "w", encoding="utf-8") as out_file:
|
|
587
|
+
json.dump(scorecard, out_file, indent=2)
|
|
588
588
|
print(f"Full telemetry report saved to: {report_path}")
|
|
589
589
|
|
|
590
590
|
return scorecard
|
|
@@ -622,7 +622,7 @@ class ApiFuzzer:
|
|
|
622
622
|
if not is_protected and method.lower() in ("post", "put", "patch", "delete"):
|
|
623
623
|
severity = "HIGH" if re.search(r"admin|internal|user", path, re.IGNORECASE) else "MEDIUM"
|
|
624
624
|
findings.append({
|
|
625
|
-
"finding":
|
|
625
|
+
"finding": "AUTH_NOT_DECLARED: Mutating endpoint declares no security requirement",
|
|
626
626
|
"severity": severity,
|
|
627
627
|
"target": label,
|
|
628
628
|
"recommendation": "Declare securitySchemes/security for this operation or document why it is public.",
|
|
@@ -678,7 +678,7 @@ class ApiFuzzer:
|
|
|
678
678
|
print(f" Verdict : {RED}[FAIL] {blocking} HIGH/CRITICAL pre-flight finding(s).{RESET}")
|
|
679
679
|
else:
|
|
680
680
|
print(f" Verdict : {GREEN}[PASS] No HIGH/CRITICAL spec-level finding.{RESET}")
|
|
681
|
-
print(
|
|
681
|
+
print("======================================================================\n")
|
|
682
682
|
for f in findings:
|
|
683
683
|
print(f" - [{f['severity']}] {f['target']} - {f['finding']}")
|
|
684
684
|
print(f" 👉 {f['recommendation']}")
|
|
@@ -694,8 +694,8 @@ class ApiFuzzer:
|
|
|
694
694
|
"severity_summary": severity_summary,
|
|
695
695
|
"findings": findings,
|
|
696
696
|
}
|
|
697
|
-
with open(report_path, "w", encoding="utf-8") as
|
|
698
|
-
json.dump(scorecard,
|
|
697
|
+
with open(report_path, "w", encoding="utf-8") as out_file:
|
|
698
|
+
json.dump(scorecard, out_file, indent=2)
|
|
699
699
|
print(f"\nFull telemetry report saved to: {report_path}")
|
|
700
700
|
|
|
701
701
|
return scorecard
|
package/tools/auto_heal.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Enterprise Closed-Loop Autonomous Test Healer Engine
|
|
4
4
|
-----------------------------------------------------
|
|
5
|
-
Diagnoses test failures in 0.1s from JUnit XML and terminal logs, queries the
|
|
5
|
+
Diagnoses test failures in 0.1s from JUnit XML and terminal logs, queries the
|
|
6
6
|
semantic memory cache for proven patches, and orchestrates targeted re-tests.
|
|
7
7
|
Dispatched automatically by AI Agents when test runs encounter failures.
|
|
8
8
|
"""
|
|
@@ -12,7 +12,6 @@ import sys
|
|
|
12
12
|
import re
|
|
13
13
|
import json
|
|
14
14
|
import argparse
|
|
15
|
-
from pathlib import Path
|
|
16
15
|
|
|
17
16
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
18
17
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -103,7 +102,7 @@ def diagnose_and_plan_heal(target_dir, junit_xml_path=None):
|
|
|
103
102
|
"heuristic_match": None,
|
|
104
103
|
"recommended_patch": None,
|
|
105
104
|
"verbal_reflection": None,
|
|
106
|
-
"episodic_lessons": [
|
|
105
|
+
"episodic_lessons": [les["lesson_summary"] for les in lessons]
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
# 2b. Reflexion Verbal Diagnosis
|
|
@@ -113,8 +112,8 @@ def diagnose_and_plan_heal(target_dir, junit_xml_path=None):
|
|
|
113
112
|
|
|
114
113
|
if lessons:
|
|
115
114
|
print(f" {CYAN}🧠 [EPISODIC REFLEXION] Top Historical Lessons for Context:{RESET}")
|
|
116
|
-
for idx,
|
|
117
|
-
print(f" {idx}. [{
|
|
115
|
+
for idx, les in enumerate(lessons, 1):
|
|
116
|
+
print(f" {idx}. [{les['page_context']}]: {les['lesson_summary']}")
|
|
118
117
|
print(f" {CYAN}💡 [REFLEXION DIAGNOSIS] {verbal_reflection}{RESET}")
|
|
119
118
|
|
|
120
119
|
if cache_solution:
|
|
@@ -24,8 +24,8 @@ import os
|
|
|
24
24
|
import sys
|
|
25
25
|
import json
|
|
26
26
|
import argparse
|
|
27
|
-
from dataclasses import dataclass
|
|
28
|
-
from typing import List, Optional, Callable,
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import List, Optional, Callable, Tuple
|
|
29
29
|
|
|
30
30
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
31
31
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -67,6 +67,14 @@ class ClosedLoopHealer:
|
|
|
67
67
|
return "Reflection: DOM node re-rendered asynchronously after API call. Adjustment: Must wait for loader/backdrop detachment before re-querying element."
|
|
68
68
|
elif "intercepted" in err_lower or "click" in err_lower:
|
|
69
69
|
return "Reflection: Click intercepted by fading overlay/spinner animation. Adjustment: Use smart wait for overlay detachment or bypass via safe JS click."
|
|
70
|
+
elif "hydration" in err_lower or "did not match" in err_lower:
|
|
71
|
+
return "Reflection: React/Next.js hydration mismatch caused DOM element to re-render. Adjustment: Ensure DOM is interactive and wait for loadState('domcontentloaded') before query."
|
|
72
|
+
elif "shadow" in err_lower or "shadowroot" in err_lower:
|
|
73
|
+
return "Reflection: Target element encapsulated inside closed Shadow DOM boundary. Adjustment: Pierce Shadow DOM via locator piercing syntax or evaluate via CDP."
|
|
74
|
+
elif "iframe" in err_lower or "frame" in err_lower:
|
|
75
|
+
return "Reflection: Target element resides inside cross-origin iframe container. Adjustment: Switch context to frameLocator('iframe#...') before clicking."
|
|
76
|
+
elif any(auth_err in err_lower for auth_err in ("401", "403", "unauthorized", "forbidden", "token expired")):
|
|
77
|
+
return "Reflection: Session auth token or cookie expired mid-test. Adjustment: Refresh storageState or trigger silent token re-authentication via API fixture."
|
|
70
78
|
elif "timeout" in err_lower or "nosuchelement" in err_lower:
|
|
71
79
|
return "Reflection: Static selector did not appear within timeout window. Adjustment: Fallback to Accessibility role/label or dynamic data-testid attribute."
|
|
72
80
|
elif "assert" in err_lower:
|
|
@@ -25,7 +25,7 @@ import json
|
|
|
25
25
|
import time
|
|
26
26
|
import argparse
|
|
27
27
|
from datetime import datetime, timezone
|
|
28
|
-
from typing import Dict, Any, List, Optional
|
|
28
|
+
from typing import Dict, Any, List, Optional
|
|
29
29
|
|
|
30
30
|
# Enforce UTF-8 stdout encoding
|
|
31
31
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -174,7 +174,7 @@ class ActorQaBroker:
|
|
|
174
174
|
if isinstance(data, list):
|
|
175
175
|
test_items = data
|
|
176
176
|
elif isinstance(data, dict):
|
|
177
|
-
test_items = data.get("tests"
|
|
177
|
+
test_items = data.get("tests") or data.get("test_cases") or []
|
|
178
178
|
except Exception as e:
|
|
179
179
|
sys.stderr.write(f"Notice: Could not parse manifest ({e}), using default mock items.\n")
|
|
180
180
|
|
package/tools/browser_helpers.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
(function () {
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
if (window.__qa && window.__qa.__version === '
|
|
16
|
+
if (window.__qa && window.__qa.__version === '3.0.1') {
|
|
17
17
|
return window.__qa;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const qa = {
|
|
50
|
-
__version: '
|
|
50
|
+
__version: '3.0.1',
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* 1. Smart Action Batching
|
package/tools/browsergym_eval.py
CHANGED
|
@@ -22,9 +22,8 @@ import sys
|
|
|
22
22
|
import re
|
|
23
23
|
import json
|
|
24
24
|
import time
|
|
25
|
-
import ast
|
|
26
25
|
import argparse
|
|
27
|
-
from typing import Dict, Any, List, Optional
|
|
26
|
+
from typing import Dict, Any, List, Optional
|
|
28
27
|
|
|
29
28
|
# Enforce UTF-8 stdout encoding
|
|
30
29
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -35,7 +34,7 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
35
34
|
pass
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
DEFAULT_BENCHMARK_TASKS = {
|
|
37
|
+
DEFAULT_BENCHMARK_TASKS: Dict[str, Any] = {
|
|
39
38
|
"ecommerce_order": {
|
|
40
39
|
"name": "E-Commerce Checkout & Order Generation",
|
|
41
40
|
"steps": [
|
package/tools/bug_reporter.py
CHANGED
|
@@ -9,11 +9,9 @@ reproduction steps, distills stacktraces (0 noise), and attaches evidence.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
12
|
import json
|
|
15
13
|
import argparse
|
|
16
|
-
from typing import Dict, List, Optional
|
|
14
|
+
from typing import Dict, List, Optional
|
|
17
15
|
from datetime import datetime
|
|
18
16
|
|
|
19
17
|
try:
|
|
@@ -22,8 +20,8 @@ except ImportError:
|
|
|
22
20
|
try:
|
|
23
21
|
from cache_resolver import compute_error_fingerprint
|
|
24
22
|
except ImportError:
|
|
25
|
-
def compute_error_fingerprint(
|
|
26
|
-
raw = f"{(
|
|
23
|
+
def compute_error_fingerprint(error_type: str, error_msg: str, stack_anchor: str = "") -> str:
|
|
24
|
+
raw = f"{(error_type or '').strip().lower()}:{(error_msg or '').strip()[:200]}"
|
|
27
25
|
h = 5381
|
|
28
26
|
for b in raw.encode("utf-8", errors="ignore"):
|
|
29
27
|
h = (((h << 5) + h) + b) & 0xFFFFFFFF
|
|
@@ -90,13 +88,13 @@ class BugReporter:
|
|
|
90
88
|
@staticmethod
|
|
91
89
|
def distill_stack_trace(raw_stack: str, max_lines: int = 5) -> str:
|
|
92
90
|
"""Filters noisy framework stack trace down to actionable lines."""
|
|
93
|
-
lines = [
|
|
91
|
+
lines = [line.strip() for line in raw_stack.splitlines() if line.strip()]
|
|
94
92
|
actionable = []
|
|
95
|
-
for
|
|
93
|
+
for line in lines:
|
|
96
94
|
# Filter standard framework internals
|
|
97
|
-
if any(internal in
|
|
95
|
+
if any(internal in line for internal in ["at Module.", "node_modules", "org.gradle", "sun.reflect", "jdk.internal"]):
|
|
98
96
|
continue
|
|
99
|
-
actionable.append(
|
|
97
|
+
actionable.append(line)
|
|
100
98
|
if len(actionable) >= max_lines:
|
|
101
99
|
break
|
|
102
100
|
return "\n".join(actionable) if actionable else raw_stack[:300]
|
|
@@ -121,7 +119,7 @@ class BugReporter:
|
|
|
121
119
|
fingerprint = compute_error_fingerprint(rca["type"], error_message, anchor)
|
|
122
120
|
|
|
123
121
|
steps = steps_executed or [
|
|
124
|
-
|
|
122
|
+
"1. Navigate to target application under test",
|
|
125
123
|
f"2. Execute test workflow for {test_title}",
|
|
126
124
|
f"3. Trigger operation causing failure: {error_message[:80]}"
|
|
127
125
|
]
|
|
@@ -144,7 +142,7 @@ class BugReporter:
|
|
|
144
142
|
report = [
|
|
145
143
|
f"h2. [BUG][{rca['severity'].upper()}] {test_id}: {test_title} - {error_message[:90]}",
|
|
146
144
|
"",
|
|
147
|
-
|
|
145
|
+
"* *Issue Type:* Bug",
|
|
148
146
|
f"* *Error Fingerprint:* {fingerprint}",
|
|
149
147
|
f"* *Root Cause Category:* {rca['label']}",
|
|
150
148
|
f"* *Severity / Priority:* {rca['severity']}",
|
|
@@ -160,7 +158,7 @@ class BugReporter:
|
|
|
160
158
|
report.extend([
|
|
161
159
|
"",
|
|
162
160
|
"h3. 2. Expected Result",
|
|
163
|
-
|
|
161
|
+
"Operation completes successfully without errors, assertions pass.",
|
|
164
162
|
"",
|
|
165
163
|
"h3. 3. Actual Result",
|
|
166
164
|
f"{{color:red}}*Failure:* {error_message}{{color}}",
|
|
@@ -199,7 +197,7 @@ class BugReporter:
|
|
|
199
197
|
"Application executes workflow smoothly and passes all validations.",
|
|
200
198
|
"",
|
|
201
199
|
"## ❌ 3. Actual Result",
|
|
202
|
-
|
|
200
|
+
"**Error Encountered:**",
|
|
203
201
|
f"```text\n{error_message}\n```",
|
|
204
202
|
"",
|
|
205
203
|
"## 🔍 4. Distilled Stack Trace (0-Noise)",
|
package/tools/bump_version.py
CHANGED
|
@@ -42,12 +42,32 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
42
42
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
43
43
|
|
|
44
44
|
def parse_semver(v_str: str):
|
|
45
|
+
"""Parses a semantic version string into major, minor, patch integers and optional suffix.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
v_str: The version string to parse (e.g. '2.9.0' or '2.9.0-rc1').
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A tuple of (major, minor, patch, suffix).
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the string does not follow semantic versioning.
|
|
55
|
+
"""
|
|
45
56
|
m = re.match(r"^(\d+)\.(\d+)\.(\d+)(.*)$", v_str.strip())
|
|
46
57
|
if not m:
|
|
47
58
|
raise ValueError(f"Invalid semver: '{v_str}'. Must follow major.minor.patch[-suffix]")
|
|
48
59
|
return int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4)
|
|
49
60
|
|
|
50
61
|
def calculate_next_version(current: str, action: str) -> str:
|
|
62
|
+
"""Computes the target version based on an action or explicit semver string.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
current: Current semver version string.
|
|
66
|
+
action: 'patch', 'minor', 'major', or an explicit target version string.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The computed next semver version string.
|
|
70
|
+
"""
|
|
51
71
|
major, minor, patch, suffix = parse_semver(current)
|
|
52
72
|
action = action.lower()
|
|
53
73
|
if action == "patch":
|
|
@@ -62,6 +82,14 @@ def calculate_next_version(current: str, action: str) -> str:
|
|
|
62
82
|
return action
|
|
63
83
|
|
|
64
84
|
def get_current_version() -> str:
|
|
85
|
+
"""Reads the current canonical version from package.json in the repository root.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The version string.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If package.json is missing or lacks a valid version field.
|
|
92
|
+
"""
|
|
65
93
|
pkg_file = os.path.join(REPO_ROOT, "package.json")
|
|
66
94
|
with open(pkg_file, "r", encoding="utf-8") as f:
|
|
67
95
|
data = json.load(f)
|
|
@@ -71,6 +99,16 @@ def get_current_version() -> str:
|
|
|
71
99
|
return version
|
|
72
100
|
|
|
73
101
|
def update_json_root_version(rel_path: str, new_ver: str, dry_run: bool = False) -> bool:
|
|
102
|
+
"""Updates the top-level 'version' property in a JSON manifest file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
rel_path: Relative path to the target JSON file from repo root.
|
|
106
|
+
new_ver: Target version string.
|
|
107
|
+
dry_run: If True, previews changes without writing to disk.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if the file was modified or would be updated.
|
|
111
|
+
"""
|
|
74
112
|
full_path = os.path.join(REPO_ROOT, rel_path)
|
|
75
113
|
if not os.path.exists(full_path):
|
|
76
114
|
print(f" [WARN] File not found: {rel_path}")
|
|
@@ -95,6 +133,17 @@ def update_json_root_version(rel_path: str, new_ver: str, dry_run: bool = False)
|
|
|
95
133
|
return True
|
|
96
134
|
|
|
97
135
|
def update_file(rel_path: str, pattern: str, replacement: str, dry_run: bool = False) -> bool:
|
|
136
|
+
"""Performs regex replacement within a target file to update version strings.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
rel_path: Relative path to the target file from repo root.
|
|
140
|
+
pattern: Regular expression pattern matching existing version occurrences.
|
|
141
|
+
replacement: String replacement containing the new version.
|
|
142
|
+
dry_run: If True, previews changes without writing to disk.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if matches were found and updated.
|
|
146
|
+
"""
|
|
98
147
|
full_path = os.path.join(REPO_ROOT, rel_path)
|
|
99
148
|
if not os.path.exists(full_path):
|
|
100
149
|
print(f" [WARN] File not found: {rel_path}")
|
|
@@ -117,6 +166,11 @@ def update_file(rel_path: str, pattern: str, replacement: str, dry_run: bool = F
|
|
|
117
166
|
return True
|
|
118
167
|
|
|
119
168
|
def run_tests() -> bool:
|
|
169
|
+
"""Executes the test suite and 10 DoD Quality Gates before release.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if all tests and gates pass.
|
|
173
|
+
"""
|
|
120
174
|
print("\n🔍 Running verification suite...")
|
|
121
175
|
cmd = [sys.executable, "-m", "unittest", "discover", "tests", "-p", "test_*.py"]
|
|
122
176
|
res = subprocess.run(cmd, cwd=REPO_ROOT)
|
|
@@ -130,33 +184,40 @@ def run_tests() -> bool:
|
|
|
130
184
|
if res_verify.returncode != 0:
|
|
131
185
|
print("❌ Quality Gates failed! Aborting release.")
|
|
132
186
|
return False
|
|
133
|
-
print("✅
|
|
187
|
+
print("✅ 10 Quality Gates DoD PASSED.")
|
|
134
188
|
return True
|
|
135
189
|
|
|
136
190
|
def git_commit_and_tag(version: str, push: bool = False):
|
|
191
|
+
"""Creates a conventional Git release commit and tags the repository.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
version: Target version string.
|
|
195
|
+
push: If True, pushes the commit and tag to git remote origin.
|
|
196
|
+
"""
|
|
137
197
|
print(f"\n📦 Creating Git commit and tag v{version}...")
|
|
138
|
-
subprocess.run(["git", "add", "-
|
|
139
|
-
subprocess.run(["git", "commit", "-m", f"chore(release): bump version to v{version}"], cwd=REPO_ROOT, check=True)
|
|
140
|
-
subprocess.run(["git", "tag", f"v{version}"], cwd=REPO_ROOT, check=True)
|
|
198
|
+
subprocess.run(["git", "add", "-u"], cwd=REPO_ROOT, check=True, timeout=30.0)
|
|
199
|
+
subprocess.run(["git", "commit", "-m", f"chore(release): bump version to v{version}"], cwd=REPO_ROOT, check=True, timeout=30.0)
|
|
200
|
+
subprocess.run(["git", "tag", f"v{version}"], cwd=REPO_ROOT, check=True, timeout=30.0)
|
|
141
201
|
print(f"✅ Git commit and tag v{version} created.")
|
|
142
202
|
|
|
143
203
|
if push:
|
|
144
|
-
print(
|
|
145
|
-
res = subprocess.run(["git", "branch", "--show-current"], cwd=REPO_ROOT, stdout=subprocess.PIPE, text=True)
|
|
204
|
+
print("🚀 Pushing commit and tags to remote origin...")
|
|
205
|
+
res = subprocess.run(["git", "branch", "--show-current"], cwd=REPO_ROOT, stdout=subprocess.PIPE, text=True, timeout=30.0)
|
|
146
206
|
cur_branch = res.stdout.strip()
|
|
147
207
|
if cur_branch == "develop":
|
|
148
208
|
try:
|
|
149
|
-
subprocess.run(["git", "checkout", "main"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
150
|
-
subprocess.run(["git", "merge", "develop", "--ff-only"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
151
|
-
subprocess.run(["git", "checkout", "develop"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
152
|
-
subprocess.run(["git", "push", "origin", "develop", "main", "--tags"], cwd=REPO_ROOT, check=True)
|
|
209
|
+
subprocess.run(["git", "checkout", "main"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30.0)
|
|
210
|
+
subprocess.run(["git", "merge", "develop", "--ff-only"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30.0)
|
|
211
|
+
subprocess.run(["git", "checkout", "develop"], cwd=REPO_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30.0)
|
|
212
|
+
subprocess.run(["git", "push", "origin", "develop", "main", "--tags"], cwd=REPO_ROOT, check=True, timeout=60.0)
|
|
153
213
|
except Exception:
|
|
154
|
-
subprocess.run(["git", "push", "origin", "HEAD", "--tags"], cwd=REPO_ROOT, check=True)
|
|
214
|
+
subprocess.run(["git", "push", "origin", "HEAD", "--tags"], cwd=REPO_ROOT, check=True, timeout=60.0)
|
|
155
215
|
else:
|
|
156
|
-
subprocess.run(["git", "push", "origin", "HEAD", "--tags"], cwd=REPO_ROOT, check=True)
|
|
157
|
-
print(
|
|
216
|
+
subprocess.run(["git", "push", "origin", "HEAD", "--tags"], cwd=REPO_ROOT, check=True, timeout=60.0)
|
|
217
|
+
print("✅ Remote origin synchronized.")
|
|
158
218
|
|
|
159
219
|
def main():
|
|
220
|
+
"""CLI entrypoint for bumping Synapse Hub version synchronously across manifests."""
|
|
160
221
|
parser = argparse.ArgumentParser(description="Atomic Universal Version Bumper for Synapse Hub")
|
|
161
222
|
parser.add_argument("action", nargs="?", default="patch", help="patch | minor | major | <explicit_version> (default: patch)")
|
|
162
223
|
parser.add_argument("--dry-run", action="store_true", help="Preview version changes without writing files")
|
|
@@ -210,15 +271,25 @@ def main():
|
|
|
210
271
|
update_file("tools/mcp_excel_server.py", r'(SERVER_VERSION\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
211
272
|
update_file("tools/mcp_excel_server.py", r'("serverInfo":\s*\{[^}]*"version":\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
212
273
|
|
|
213
|
-
# 8. docs/ARCHITECTURE.md
|
|
274
|
+
# 8. docs/ARCHITECTURE.md & docs/ARCHITECTURE.en.md
|
|
214
275
|
update_file("docs/ARCHITECTURE.md", r'(>\s*\*\*Version:\*\*\s*`)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
276
|
+
update_file("docs/ARCHITECTURE.en.md", r'(>\s*\*\*Version:\*\*\s*`)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
215
277
|
|
|
216
278
|
# 9. README.md & README.en.md (Documentation headings and badges)
|
|
217
|
-
update_file("README.md", r'(Phiên bản Hub
|
|
218
|
-
update_file("README.md", r'(Hub Version:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
279
|
+
update_file("README.md", r'(Phiên bản(?: Hub)?:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
280
|
+
update_file("README.md", r'((?:Hub )?Version:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
219
281
|
update_file("README.md", r'(Intelligence Core v)[^")\]]+', rf"\g<1>{new_ver}", args.dry_run)
|
|
220
|
-
update_file("README.
|
|
282
|
+
update_file("README.md", r'(rev:\s*v)[0-9]+\.[0-9]+\.[0-9]+', rf"\g<1>{new_ver}", args.dry_run)
|
|
283
|
+
update_file("README.md", r'(Synapse Hub \(Core v)[0-9]+\.[0-9]+\.[0-9]+(\))', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
284
|
+
update_file("README.en.md", r'((?:Hub )?Version:\*\*\s*`v)[^`]+(`)', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
221
285
|
update_file("README.en.md", r'(Intelligence Core v)[^")\]]+', rf"\g<1>{new_ver}", args.dry_run)
|
|
286
|
+
update_file("README.en.md", r'(rev:\s*v)[0-9]+\.[0-9]+\.[0-9]+', rf"\g<1>{new_ver}", args.dry_run)
|
|
287
|
+
update_file("README.en.md", r'(Synapse Hub \(Core v)[0-9]+\.[0-9]+\.[0-9]+(\))', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
288
|
+
|
|
289
|
+
# 9b. SECURITY.md
|
|
290
|
+
major, minor, _, _ = parse_semver(new_ver)
|
|
291
|
+
current_branch = f"{major}.{minor}.x"
|
|
292
|
+
update_file("SECURITY.md", r'(\|\s*`)[0-9]+\.[0-9]+\.x(`\s*\|\s*✅)', rf"\g<1>{current_branch}\g<2>", args.dry_run)
|
|
222
293
|
|
|
223
294
|
# 10. tools/__init__.py
|
|
224
295
|
update_file("tools/__init__.py", r'(__version__\s*=\s*")[^"]+(")', rf"\g<1>{new_ver}\g<2>", args.dry_run)
|
|
@@ -267,8 +338,8 @@ def main():
|
|
|
267
338
|
print(f"🎉 VERSION BUMP COMPLETE: v{current_ver} ➔ v{new_ver}")
|
|
268
339
|
print("======================================================================")
|
|
269
340
|
print("Next steps:")
|
|
270
|
-
print(
|
|
271
|
-
print(
|
|
341
|
+
print(" • Publish to NPM : npm publish")
|
|
342
|
+
print(" • Or Git Push : git push origin HEAD --tags")
|
|
272
343
|
print("======================================================================")
|
|
273
344
|
|
|
274
345
|
if __name__ == "__main__":
|