synapse-hub 0.0.0-stage → 2.0.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 +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +394 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +418 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fast Zero-Token Failure Pinpointer for Katalon & Jenkins Reports.
|
|
4
|
+
Scans the latest Reports/**/JUnit_Report.xml, extracts failed Test Cases,
|
|
5
|
+
exact failed lines in Groovy/Script, and the root-cause exception.
|
|
6
|
+
Outputs < 100 tokens.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 tools/pinpoint_failure.py
|
|
10
|
+
python3 tools/pinpoint_failure.py path/to/JUnit_Report.xml
|
|
11
|
+
"""
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import glob
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
def find_latest_report():
|
|
19
|
+
reports = glob.glob("Reports/**/JUnit_Report.xml", recursive=True)
|
|
20
|
+
if not reports:
|
|
21
|
+
return None
|
|
22
|
+
reports.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
|
23
|
+
return reports[0]
|
|
24
|
+
|
|
25
|
+
def parse_failures(xml_path):
|
|
26
|
+
if not os.path.exists(xml_path):
|
|
27
|
+
print(f"Error: {xml_path} does not exist.")
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
tree = ET.parse(xml_path)
|
|
31
|
+
root = tree.getroot()
|
|
32
|
+
failures = []
|
|
33
|
+
|
|
34
|
+
suites = [root] if root.tag == "testsuite" else root.findall("testsuite")
|
|
35
|
+
|
|
36
|
+
for suite in suites:
|
|
37
|
+
for tc in suite.findall("testcase"):
|
|
38
|
+
tc_name = tc.get("name", "Unknown")
|
|
39
|
+
tc_time = tc.get("time", "0")
|
|
40
|
+
fail = tc.find("failure")
|
|
41
|
+
err = tc.find("error")
|
|
42
|
+
target = fail if fail is not None else err
|
|
43
|
+
|
|
44
|
+
if target is not None:
|
|
45
|
+
msg = target.get("message", "")
|
|
46
|
+
text = target.text or ""
|
|
47
|
+
combined = (msg + "\n" + text).replace("
", "\n")
|
|
48
|
+
|
|
49
|
+
# Extract Root Cause Reason
|
|
50
|
+
reason_match = re.search(r"Reason:\s*(.+?)(?=\n\s*at|\Z)", combined, re.DOTALL)
|
|
51
|
+
if reason_match:
|
|
52
|
+
reason = reason_match.group(1).strip()
|
|
53
|
+
else:
|
|
54
|
+
lines = [l.strip() for l in combined.split("\n") if l.strip()]
|
|
55
|
+
reason = lines[0] if lines else "Unknown Error"
|
|
56
|
+
|
|
57
|
+
# Extract Failed Line in Groovy Page / Script
|
|
58
|
+
Enterprise_lines = re.findall(r"(?:Enterprise\.pages\.[\w]+|TC-[\w\s\u00C0-\u1EF9]+|Script\d+)\.groovy:\d+|(?:TC-[\w\s\u00C0-\u1EF9]+:\d+)", combined)
|
|
59
|
+
|
|
60
|
+
failures.append({
|
|
61
|
+
"tc": tc_name.split("/")[-1],
|
|
62
|
+
"time": f"{float(tc_time):.1f}s",
|
|
63
|
+
"reason": reason.split("\n")[0][:180],
|
|
64
|
+
"failed_at": Enterprise_lines[:2] if Enterprise_lines else ["See JUnit report"]
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return failures
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
xml_path = sys.argv[1] if len(sys.argv) > 1 else find_latest_report()
|
|
71
|
+
if not xml_path:
|
|
72
|
+
print("No JUnit_Report.xml found in Reports/.")
|
|
73
|
+
sys.exit(0)
|
|
74
|
+
|
|
75
|
+
print(f"=== Report: {xml_path} ===")
|
|
76
|
+
failures = parse_failures(xml_path)
|
|
77
|
+
if not failures:
|
|
78
|
+
print("✅ 100% PASSED: Zero failures found in report.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
print(f"❌ Found {len(failures)} Failed Test Case(s):")
|
|
82
|
+
for i, f in enumerate(failures, 1):
|
|
83
|
+
print(f"[{i}] {f['tc']} ({f['time']})")
|
|
84
|
+
print(f" Cause: {f['reason']}")
|
|
85
|
+
print(f" Line: {', '.join(f['failed_at'])}")
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Project State Machine & Token-Capped Memory Anchor
|
|
4
|
+
--------------------------------------------------------------
|
|
5
|
+
Maintains persistent execution state across long-running AI sessions and Antigravity context compaction.
|
|
6
|
+
Enforces a strict Rolling Window (< 50 lines, ~200 tokens) to guarantee zero token bloat.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
16
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
17
|
+
try:
|
|
18
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
19
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
25
|
+
TEMPLATES_DIR = os.path.join(HUB_ROOT, "templates")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_state_file(target_dir):
|
|
29
|
+
return os.path.join(target_dir, "PROJECT_STATE.md")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_state(target_dir, goal="Initial Autonomous Setup"):
|
|
33
|
+
"""Initializes PROJECT_STATE.md if missing, strictly capped to < 50 lines."""
|
|
34
|
+
state_file = resolve_state_file(target_dir)
|
|
35
|
+
if os.path.exists(state_file):
|
|
36
|
+
return state_file
|
|
37
|
+
|
|
38
|
+
project_name = os.path.basename(os.path.abspath(target_dir))
|
|
39
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
40
|
+
|
|
41
|
+
content = f"""# Project Execution State (`PROJECT_STATE.md`)
|
|
42
|
+
|
|
43
|
+
- **Project:** `{project_name}`
|
|
44
|
+
- **Last Sync:** `{timestamp}`
|
|
45
|
+
- **Overall Status:** `READY`
|
|
46
|
+
|
|
47
|
+
## 1. Active Goal
|
|
48
|
+
- {goal}
|
|
49
|
+
|
|
50
|
+
## 2. Task Checklist (Rolling Window - Max 8 active items)
|
|
51
|
+
- [x] Initialized project state anchor
|
|
52
|
+
- [ ] Ready for feature or test automation
|
|
53
|
+
|
|
54
|
+
## 3. Active Watchpoints & Healing (Max 3 items)
|
|
55
|
+
- Target Suite / Script: `[None]`
|
|
56
|
+
- Failing Locator / Line: `[None]`
|
|
57
|
+
|
|
58
|
+
## 4. Quality Verification
|
|
59
|
+
- 7 Quality Gates: `READY`
|
|
60
|
+
- Clean POM Integrity: `VERIFIED`
|
|
61
|
+
"""
|
|
62
|
+
with open(state_file, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(content)
|
|
64
|
+
return state_file
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def update_state(target_dir, goal=None, completed_task=None, pending_task=None, watchpoint=None, gate_status=None):
|
|
68
|
+
"""Updates state in-place while pruning old entries to guarantee < 50 lines (~200 tokens)."""
|
|
69
|
+
state_file = resolve_state_file(target_dir)
|
|
70
|
+
if not os.path.exists(state_file):
|
|
71
|
+
init_state(target_dir, goal or "Active Task")
|
|
72
|
+
|
|
73
|
+
with open(state_file, "r", encoding="utf-8") as f:
|
|
74
|
+
lines = [line.rstrip() for line in f.readlines()]
|
|
75
|
+
|
|
76
|
+
project_name = os.path.basename(os.path.abspath(target_dir))
|
|
77
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
78
|
+
|
|
79
|
+
# Parse existing tasks
|
|
80
|
+
existing_tasks = []
|
|
81
|
+
current_goal = goal or "Continue autonomous QA workflow"
|
|
82
|
+
current_watchpoints = []
|
|
83
|
+
current_gate = gate_status or "READY"
|
|
84
|
+
|
|
85
|
+
in_tasks = False
|
|
86
|
+
in_watch = False
|
|
87
|
+
|
|
88
|
+
for line in lines:
|
|
89
|
+
if line.startswith("## 1. Active Goal"):
|
|
90
|
+
in_tasks = False
|
|
91
|
+
in_watch = False
|
|
92
|
+
elif line.startswith("## 2. Task Checklist"):
|
|
93
|
+
in_tasks = True
|
|
94
|
+
in_watch = False
|
|
95
|
+
continue
|
|
96
|
+
elif line.startswith("## 3. Active Watchpoints"):
|
|
97
|
+
in_tasks = False
|
|
98
|
+
in_watch = True
|
|
99
|
+
continue
|
|
100
|
+
elif line.startswith("## 4. Quality Verification"):
|
|
101
|
+
in_tasks = False
|
|
102
|
+
in_watch = False
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
if in_tasks and line.strip().startswith("- ["):
|
|
106
|
+
existing_tasks.append(line.strip())
|
|
107
|
+
elif in_watch and line.strip().startswith("- "):
|
|
108
|
+
current_watchpoints.append(line.strip())
|
|
109
|
+
elif line.startswith("- **Last Sync:**") and not goal:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
if completed_task:
|
|
113
|
+
existing_tasks.append(f"- [x] {completed_task}")
|
|
114
|
+
if pending_task:
|
|
115
|
+
existing_tasks.append(f"- [ ] {pending_task}")
|
|
116
|
+
if watchpoint:
|
|
117
|
+
current_watchpoints = [f"- Focus: {watchpoint}"]
|
|
118
|
+
|
|
119
|
+
# Rolling window cap: Keep maximum 8 task items (purge oldest completed tasks)
|
|
120
|
+
if len(existing_tasks) > 8:
|
|
121
|
+
# Keep pending tasks and the most recent completed ones
|
|
122
|
+
pending = [t for t in existing_tasks if t.startswith("- [ ]")]
|
|
123
|
+
completed = [t for t in existing_tasks if t.startswith("- [x]")]
|
|
124
|
+
# Keep up to 4 completed and remaining pending
|
|
125
|
+
completed = completed[-4:]
|
|
126
|
+
existing_tasks = completed + pending
|
|
127
|
+
existing_tasks = existing_tasks[-8:]
|
|
128
|
+
|
|
129
|
+
# Reconstruct compact markdown (< 40 lines)
|
|
130
|
+
tasks_block = "\n".join(existing_tasks) if existing_tasks else "- [ ] Standby for instructions"
|
|
131
|
+
watch_block = "\n".join(current_watchpoints[:3]) if current_watchpoints else "- Target Suite / Script: `[None]`\n- Failing Locator / Line: `[None]`"
|
|
132
|
+
|
|
133
|
+
content = f"""# Project Execution State (`PROJECT_STATE.md`)
|
|
134
|
+
|
|
135
|
+
- **Project:** `{project_name}`
|
|
136
|
+
- **Last Sync:** `{timestamp}`
|
|
137
|
+
- **Overall Status:** `IN_PROGRESS`
|
|
138
|
+
|
|
139
|
+
## 1. Active Goal
|
|
140
|
+
- {current_goal}
|
|
141
|
+
|
|
142
|
+
## 2. Task Checklist (Rolling Window - Max 8 active items)
|
|
143
|
+
{tasks_block}
|
|
144
|
+
|
|
145
|
+
## 3. Active Watchpoints & Healing (Max 3 items)
|
|
146
|
+
{watch_block}
|
|
147
|
+
|
|
148
|
+
## 4. Quality Verification
|
|
149
|
+
- 7 Quality Gates: `{current_gate}`
|
|
150
|
+
- Clean POM Integrity: `VERIFIED`
|
|
151
|
+
"""
|
|
152
|
+
with open(state_file, "w", encoding="utf-8") as f:
|
|
153
|
+
f.write(content)
|
|
154
|
+
return state_file
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_state(target_dir):
|
|
158
|
+
"""Reads state file if exists, returning condensed text."""
|
|
159
|
+
state_file = resolve_state_file(target_dir)
|
|
160
|
+
if not os.path.exists(state_file):
|
|
161
|
+
return None
|
|
162
|
+
with open(state_file, "r", encoding="utf-8") as f:
|
|
163
|
+
return f.read()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
target = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
|
|
168
|
+
sf = init_state(target)
|
|
169
|
+
print(f"Project state anchor initialized at: {sf}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generated_at": "2026-10-29 19:59:47",
|
|
3
|
+
"target_repository": "/Users/phungcao/Documents/Agent Skill",
|
|
4
|
+
"classes_count": 0,
|
|
5
|
+
"methods_count": 0,
|
|
6
|
+
"source_lines": 0,
|
|
7
|
+
"raw_bytes": 0,
|
|
8
|
+
"compressed_bytes": 442,
|
|
9
|
+
"compression_ratio_pct": -44100.0,
|
|
10
|
+
"estimated_tokens": 110,
|
|
11
|
+
"classes": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enterprise Repository Map Compressor (Inspired by Aider-AI/aider)
|
|
4
|
+
-----------------------------------------------------------
|
|
5
|
+
Extracts an ultra-compact, token-budgeted AST map of all Page Classes and Helpers.
|
|
6
|
+
Compresses hundreds of kilobytes of Groovy source code into a clean ~150-line summary.
|
|
7
|
+
Enables AI Agents to discover all available methods and signatures with < 600 tokens.
|
|
8
|
+
Supports dynamic target repository resolution (CLI flag, CWD auto-detect, ENV var).
|
|
9
|
+
Saves a persistent in-place copy (repo_map.txt) inside the target project directory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import re
|
|
15
|
+
import json
|
|
16
|
+
import argparse
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
20
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
21
|
+
OUTPUT_TXT = os.path.join(SCRIPT_DIR, "repo_map.txt")
|
|
22
|
+
OUTPUT_JSON = os.path.join(SCRIPT_DIR, "repo_map.json")
|
|
23
|
+
|
|
24
|
+
# Regex patterns for Groovy classes and methods
|
|
25
|
+
CLASS_PATTERN = re.compile(r'^\s*(?:public\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', re.MULTILINE)
|
|
26
|
+
METHOD_PATTERN = re.compile(
|
|
27
|
+
r'^\s*(?:@\w+\s+)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:def\s+|void\s+|[\w<>\[\],]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*\{',
|
|
28
|
+
re.MULTILINE
|
|
29
|
+
)
|
|
30
|
+
DOC_PATTERN = re.compile(r'/\*\*(.*?)\*/', re.DOTALL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
34
|
+
"""
|
|
35
|
+
Resolves the target repository path for repo mapping:
|
|
36
|
+
1. Explicit CLI flag: --repo / --path or positional target.
|
|
37
|
+
2. Environment variable: TARGET_PROJECT_DIR.
|
|
38
|
+
3. Current Working Directory (if different from Hub and has Keywords/ or Scripts/).
|
|
39
|
+
4. Default fallback: QA Hub root (parent of tools/).
|
|
40
|
+
"""
|
|
41
|
+
chosen = cli_repo or cli_target
|
|
42
|
+
if chosen:
|
|
43
|
+
resolved = os.path.abspath(chosen)
|
|
44
|
+
if not os.path.exists(resolved):
|
|
45
|
+
raise FileNotFoundError(f"Target repository path not found: {resolved}")
|
|
46
|
+
return resolved
|
|
47
|
+
|
|
48
|
+
env_dir = os.environ.get("TARGET_PROJECT_DIR")
|
|
49
|
+
if env_dir and os.path.exists(env_dir):
|
|
50
|
+
return os.path.abspath(env_dir)
|
|
51
|
+
|
|
52
|
+
cwd = os.path.abspath(os.getcwd())
|
|
53
|
+
if cwd.lower() != HUB_ROOT.lower():
|
|
54
|
+
if os.path.exists(os.path.join(cwd, "Keywords")) or os.path.exists(os.path.join(cwd, "Scripts")):
|
|
55
|
+
return cwd
|
|
56
|
+
|
|
57
|
+
return HUB_ROOT
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def extract_class_info(file_path, repo_root=None):
|
|
61
|
+
"""Parses a Groovy file and extracts class name, parent, and exported methods."""
|
|
62
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
63
|
+
content = f.read()
|
|
64
|
+
|
|
65
|
+
base_root = repo_root or HUB_ROOT
|
|
66
|
+
try:
|
|
67
|
+
rel_path = os.path.relpath(file_path, base_root)
|
|
68
|
+
except ValueError:
|
|
69
|
+
rel_path = os.path.abspath(file_path)
|
|
70
|
+
|
|
71
|
+
class_match = CLASS_PATTERN.search(content)
|
|
72
|
+
if not class_match:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
class_name = class_match.group(1)
|
|
76
|
+
extends_name = class_match.group(2) or ""
|
|
77
|
+
|
|
78
|
+
methods = []
|
|
79
|
+
control_keywords = {"if", "for", "while", "switch", "catch", "synchronized"}
|
|
80
|
+
|
|
81
|
+
lines = content.splitlines()
|
|
82
|
+
for idx, line in enumerate(lines):
|
|
83
|
+
line_clean = line.strip()
|
|
84
|
+
if not line_clean or line_clean.startswith("//") or line_clean.startswith("/*") or line_clean.startswith("*"):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# If '=' appears before '(', it's a variable assignment / constant declaration, not a method
|
|
88
|
+
if "=" in line_clean:
|
|
89
|
+
eq_pos = line_clean.find("=")
|
|
90
|
+
paren_pos = line_clean.find("(")
|
|
91
|
+
if paren_pos != -1 and eq_pos < paren_pos:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Must have parentheses and end with '{' or next line starts with '{'
|
|
95
|
+
if "(" in line_clean and ")" in line_clean:
|
|
96
|
+
has_brace = line_clean.endswith("{") or (idx + 1 < len(lines) and lines[idx + 1].strip().startswith("{"))
|
|
97
|
+
if has_brace:
|
|
98
|
+
# Match method definition: modifiers + return type + name + (params)
|
|
99
|
+
m = re.search(r'(?:public\s+|protected\s+|private\s+|static\s+|def\s+|void\s+|[\w<>\[\],]+\s+)+([a-zA-Z_]\w*)\s*\(([^)]*)\)', line_clean)
|
|
100
|
+
if m:
|
|
101
|
+
name = m.group(1)
|
|
102
|
+
params = m.group(2).strip()
|
|
103
|
+
if name not in control_keywords and name != class_name:
|
|
104
|
+
# Extract preceding single-line comment if any
|
|
105
|
+
comment = ""
|
|
106
|
+
if idx > 0 and lines[idx - 1].strip().startswith("//"):
|
|
107
|
+
comment = lines[idx - 1].strip().lstrip("/ ").strip()
|
|
108
|
+
methods.append({
|
|
109
|
+
"name": name,
|
|
110
|
+
"params": re.sub(r'\s+', ' ', params),
|
|
111
|
+
"comment": comment
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
# Deduplicate methods preserving order
|
|
115
|
+
seen = set()
|
|
116
|
+
unique_methods = []
|
|
117
|
+
for meth in methods:
|
|
118
|
+
sig = f"{meth['name']}({meth['params']})"
|
|
119
|
+
if sig not in seen:
|
|
120
|
+
seen.add(sig)
|
|
121
|
+
unique_methods.append(meth)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"rel_path": rel_path,
|
|
125
|
+
"class_name": class_name,
|
|
126
|
+
"extends": extends_name,
|
|
127
|
+
"raw_size_bytes": len(content.encode("utf-8")),
|
|
128
|
+
"line_count": len(lines),
|
|
129
|
+
"methods": unique_methods
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
134
|
+
"""Scans Keywords in target_repo and builds the condensed repo map."""
|
|
135
|
+
repo = target_repo or resolve_target_repo()
|
|
136
|
+
txt_dest = output_txt or OUTPUT_TXT
|
|
137
|
+
json_dest = output_json or OUTPUT_JSON
|
|
138
|
+
|
|
139
|
+
classes = []
|
|
140
|
+
total_raw_bytes = 0
|
|
141
|
+
total_raw_lines = 0
|
|
142
|
+
|
|
143
|
+
keywords_dir = os.path.join(repo, "Keywords")
|
|
144
|
+
|
|
145
|
+
if os.path.isdir(keywords_dir):
|
|
146
|
+
for root, dirs, files in os.walk(keywords_dir):
|
|
147
|
+
for fname in sorted(files):
|
|
148
|
+
if fname.endswith(".groovy"):
|
|
149
|
+
full_path = os.path.join(root, fname)
|
|
150
|
+
info = extract_class_info(full_path, repo_root=repo)
|
|
151
|
+
if info:
|
|
152
|
+
classes.append(info)
|
|
153
|
+
total_raw_bytes += info["raw_size_bytes"]
|
|
154
|
+
total_raw_lines += info["line_count"]
|
|
155
|
+
|
|
156
|
+
# Sort classes: BasePage first, then alphabetical
|
|
157
|
+
classes.sort(key=lambda c: (0 if c["class_name"] == "BasePage" else 1, c["class_name"]))
|
|
158
|
+
|
|
159
|
+
# Render text map
|
|
160
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
161
|
+
out_lines = [
|
|
162
|
+
f"# ==============================================================================",
|
|
163
|
+
f"# ENTERPRISE AUTOMATION REPOSITORY MAP (Aider-Style AST Compression)",
|
|
164
|
+
f"# Generated : {timestamp}",
|
|
165
|
+
f"# Target : {repo}",
|
|
166
|
+
f"# Scope : {len(classes)} Classes ({total_raw_lines} source lines, {total_raw_bytes / 1024:.1f} KB)",
|
|
167
|
+
f"# Purpose : Token-budgeted index (< 600 tokens) for instant method lookup",
|
|
168
|
+
f"# ==============================================================================\n"
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
total_methods = 0
|
|
172
|
+
for cls in classes:
|
|
173
|
+
ext_str = f" extends {cls['extends']}" if cls['extends'] else ""
|
|
174
|
+
out_lines.append(f"{cls['rel_path']} ({cls['line_count']} lines)")
|
|
175
|
+
out_lines.append(f" class {cls['class_name']}{ext_str}:")
|
|
176
|
+
for m in cls["methods"]:
|
|
177
|
+
total_methods += 1
|
|
178
|
+
cmt = f" # {m['comment']}" if m['comment'] else ""
|
|
179
|
+
out_lines.append(f" def {m['name']}({m['params']}){cmt}")
|
|
180
|
+
out_lines.append("")
|
|
181
|
+
|
|
182
|
+
rendered_text = "\n".join(out_lines)
|
|
183
|
+
compressed_bytes = len(rendered_text.encode("utf-8"))
|
|
184
|
+
ratio = (1.0 - (compressed_bytes / max(1, total_raw_bytes))) * 100
|
|
185
|
+
est_tokens = len(rendered_text) // 4
|
|
186
|
+
|
|
187
|
+
# 1. Save to primary destination (tools/repo_map.txt in Hub)
|
|
188
|
+
os.makedirs(os.path.dirname(txt_dest), exist_ok=True)
|
|
189
|
+
with open(txt_dest, "w", encoding="utf-8") as f:
|
|
190
|
+
f.write(rendered_text)
|
|
191
|
+
|
|
192
|
+
os.makedirs(os.path.dirname(json_dest), exist_ok=True)
|
|
193
|
+
with open(json_dest, "w", encoding="utf-8") as f:
|
|
194
|
+
json.dump({
|
|
195
|
+
"generated_at": timestamp,
|
|
196
|
+
"target_repository": repo,
|
|
197
|
+
"classes_count": len(classes),
|
|
198
|
+
"methods_count": total_methods,
|
|
199
|
+
"source_lines": total_raw_lines,
|
|
200
|
+
"raw_bytes": total_raw_bytes,
|
|
201
|
+
"compressed_bytes": compressed_bytes,
|
|
202
|
+
"compression_ratio_pct": round(ratio, 2),
|
|
203
|
+
"estimated_tokens": est_tokens,
|
|
204
|
+
"classes": classes
|
|
205
|
+
}, f, indent=2, ensure_ascii=False)
|
|
206
|
+
|
|
207
|
+
# 2. Save a persistent copy directly into target repository root (for AI self-contained inspection)
|
|
208
|
+
if os.path.abspath(repo).lower() != os.path.abspath(HUB_ROOT).lower():
|
|
209
|
+
try:
|
|
210
|
+
target_txt = os.path.join(repo, "repo_map.txt")
|
|
211
|
+
with open(target_txt, "w", encoding="utf-8") as f:
|
|
212
|
+
f.write(rendered_text)
|
|
213
|
+
target_json = os.path.join(repo, "repo_map.json")
|
|
214
|
+
with open(target_json, "w", encoding="utf-8") as f:
|
|
215
|
+
json.dump({
|
|
216
|
+
"generated_at": timestamp,
|
|
217
|
+
"target_repository": repo,
|
|
218
|
+
"classes_count": len(classes),
|
|
219
|
+
"methods_count": total_methods,
|
|
220
|
+
"source_lines": total_raw_lines,
|
|
221
|
+
"raw_bytes": total_raw_bytes,
|
|
222
|
+
"compressed_bytes": compressed_bytes,
|
|
223
|
+
"compression_ratio_pct": round(ratio, 2),
|
|
224
|
+
"estimated_tokens": est_tokens,
|
|
225
|
+
"classes": classes
|
|
226
|
+
}, f, indent=2, ensure_ascii=False)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"target_repo": repo,
|
|
232
|
+
"classes_count": len(classes),
|
|
233
|
+
"methods_count": total_methods,
|
|
234
|
+
"source_lines": total_raw_lines,
|
|
235
|
+
"raw_bytes": total_raw_bytes,
|
|
236
|
+
"compressed_bytes": compressed_bytes,
|
|
237
|
+
"compression_ratio_pct": ratio,
|
|
238
|
+
"estimated_tokens": est_tokens,
|
|
239
|
+
"output_file": txt_dest
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
parser = argparse.ArgumentParser(description="Enterprise Repository Map Compressor")
|
|
245
|
+
parser.add_argument("target", nargs="?", default=None, help="Target repository containing Keywords/")
|
|
246
|
+
parser.add_argument("--repo", "--path", dest="repo_path", default=None, help="Target repository path")
|
|
247
|
+
parser.add_argument("--print", action="store_true", help="Print repository map to console")
|
|
248
|
+
args = parser.parse_args()
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
target_repo = resolve_target_repo(cli_target=args.target, cli_repo=args.repo_path)
|
|
252
|
+
except FileNotFoundError as e:
|
|
253
|
+
print(f"❌ Error: {e}")
|
|
254
|
+
return 1
|
|
255
|
+
|
|
256
|
+
stats = build_repo_map(target_repo=target_repo)
|
|
257
|
+
print(f"\n======================================================================")
|
|
258
|
+
print(f" Enterprise Repository Map Compressor (Aider-Style AST Map) ")
|
|
259
|
+
print(f"======================================================================")
|
|
260
|
+
print(f" Target Repository : {stats['target_repo']}")
|
|
261
|
+
print(f" Indexed Classes : {stats['classes_count']} Page Classes & Helpers")
|
|
262
|
+
print(f" Extracted Methods : {stats['methods_count']} API signatures")
|
|
263
|
+
print(f" Source Lines Processed : {stats['source_lines']:,} lines ({stats['raw_bytes'] / 1024:.1f} KB)")
|
|
264
|
+
print(f" Compressed Map Size : {stats['compressed_bytes'] / 1024:.1f} KB (~{stats['estimated_tokens']} tokens)")
|
|
265
|
+
print(f" Compression Ratio : {stats['compression_ratio_pct']:.1f}% reduction")
|
|
266
|
+
print(f" Artifact Generated : {stats['output_file']}")
|
|
267
|
+
print(f"======================================================================\n")
|
|
268
|
+
|
|
269
|
+
if args.print:
|
|
270
|
+
with open(stats['output_file'], "r", encoding="utf-8") as f:
|
|
271
|
+
print(f.read())
|
|
272
|
+
|
|
273
|
+
return 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
sys.exit(main())
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# ENTERPRISE AUTOMATION REPOSITORY MAP (Aider-Style AST Compression)
|
|
3
|
+
# Generated : 2026-10-29 19:59:47
|
|
4
|
+
# Target : /Users/phungcao/Documents/Agent Skill
|
|
5
|
+
# Scope : 0 Classes (0 source lines, 0.0 KB)
|
|
6
|
+
# Purpose : Token-budgeted index (< 600 tokens) for instant method lookup
|
|
7
|
+
# ==============================================================================
|