synapse-hub 3.0.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/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/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/review-spec/examples/pocock_grilling_decision_rubric.md +40 -0
- 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/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 +1 -0
- package/README.en.md +8 -8
- package/README.md +3 -3
- package/action.yml +1 -1
- package/bin/agent-qa.js +2 -2
- package/docs/ARCHITECTURE.en.md +192 -0
- package/docs/ARCHITECTURE.md +127 -126
- 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 +25 -67
- package/docs/QUALITY_GATES.en.md +115 -0
- package/docs/QUALITY_GATES.md +82 -80
- package/docs/research/agent_skill_training_analysis.md +1 -1
- package/package.json +1 -1
- package/skills_manifest.json +1 -1
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +1 -0
- package/tools/__init__.py +1 -1
- package/tools/auto_heal_reflexion.py +8 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/bump_version.py +15 -14
- package/tools/check_version_sync.py +24 -7
- package/tools/cli.py +3 -3
- package/tools/contract_validator.py +141 -0
- package/tools/dashboard_generator.py +11 -11
- package/tools/db_sandbox.py +22 -9
- package/tools/generate_allure_report.py +1 -1
- package/tools/infer_linter.py +135 -21
- package/tools/mcp_excel_server.py +1 -1
- package/tools/mcp_server.py +7 -4
- package/tools/micro_mutator.py +21 -3
- package/tools/reversible_sandbox.py +2 -2
- package/tools/senior_code_review.py +2 -1
- package/tools/skill_vault.py +82 -3
- package/tools/skillspector.py +5 -1
- package/tools/sync_agent_rules.py +1 -0
- package/tools/test_impact.py +2 -1
- package/tools/update_sentinel.py +3 -3
- package/tools/verify_all.py +2 -2
- package/tools/visual_diff_canvas.py +3 -2
- package/tools/welcome_page.py +1 -1
|
@@ -102,7 +102,7 @@ def extract_versions(repo_root: str) -> Dict[str, str]:
|
|
|
102
102
|
if m:
|
|
103
103
|
versions["tools/mcp_excel_server.py"] = m.group(1)
|
|
104
104
|
|
|
105
|
-
# 10. docs/ARCHITECTURE.md
|
|
105
|
+
# 10. docs/ARCHITECTURE.md & docs/ARCHITECTURE.en.md
|
|
106
106
|
arch_path = os.path.join(repo_root, "docs", "ARCHITECTURE.md")
|
|
107
107
|
if os.path.exists(arch_path):
|
|
108
108
|
with open(arch_path, "r", encoding="utf-8") as f:
|
|
@@ -110,24 +110,41 @@ def extract_versions(repo_root: str) -> Dict[str, str]:
|
|
|
110
110
|
m = re.search(r'>\s*\*\*Version:\*\*\s*`([^`]+)`', content)
|
|
111
111
|
if m:
|
|
112
112
|
versions["docs/ARCHITECTURE.md"] = m.group(1)
|
|
113
|
+
arch_en_path = os.path.join(repo_root, "docs", "ARCHITECTURE.en.md")
|
|
114
|
+
if os.path.exists(arch_en_path):
|
|
115
|
+
with open(arch_en_path, "r", encoding="utf-8") as f:
|
|
116
|
+
content = f.read()
|
|
117
|
+
m = re.search(r'>\s*\*\*Version:\*\*\s*`([^`]+)`', content)
|
|
118
|
+
if m:
|
|
119
|
+
versions["docs/ARCHITECTURE.en.md"] = m.group(1)
|
|
113
120
|
|
|
114
121
|
# 11. README.md
|
|
115
122
|
readme_path = os.path.join(repo_root, "README.md")
|
|
116
123
|
if os.path.exists(readme_path):
|
|
117
124
|
with open(readme_path, "r", encoding="utf-8") as f:
|
|
118
125
|
content = f.read()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
m_badge = re.search(r'Phiên bản(?: Hub)?:\*\*\s*`v([0-9]+\.[0-9]+\.[0-9]+)', content)
|
|
127
|
+
m_rev = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
|
|
128
|
+
if m_badge:
|
|
129
|
+
versions["README.md"] = m_badge.group(1)
|
|
130
|
+
elif m_rev:
|
|
131
|
+
versions["README.md"] = m_rev.group(1)
|
|
132
|
+
if m_badge and m_rev:
|
|
133
|
+
versions["README.md (rev)"] = m_rev.group(1)
|
|
122
134
|
|
|
123
135
|
# 12. README.en.md
|
|
124
136
|
readme_en_path = os.path.join(repo_root, "README.en.md")
|
|
125
137
|
if os.path.exists(readme_en_path):
|
|
126
138
|
with open(readme_en_path, "r", encoding="utf-8") as f:
|
|
127
139
|
content = f.read()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
m_badge = re.search(r'(?:Hub )?Version:\*\*\s*`v([0-9]+\.[0-9]+\.[0-9]+)', content)
|
|
141
|
+
m_rev = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
|
|
142
|
+
if m_badge:
|
|
143
|
+
versions["README.en.md"] = m_badge.group(1)
|
|
144
|
+
elif m_rev:
|
|
145
|
+
versions["README.en.md"] = m_rev.group(1)
|
|
146
|
+
if m_badge and m_rev:
|
|
147
|
+
versions["README.en.md (rev)"] = m_rev.group(1)
|
|
131
148
|
|
|
132
149
|
# 13. SECURITY.md
|
|
133
150
|
sec_path = os.path.join(repo_root, "SECURITY.md")
|
package/tools/cli.py
CHANGED
|
@@ -41,7 +41,7 @@ RESET = "\033[0m"
|
|
|
41
41
|
def cmd_version(args):
|
|
42
42
|
"""Displays the current hub version, skill catalog count, and license information."""
|
|
43
43
|
manifest_path = os.path.join(REPO_ROOT, "skills_manifest.json")
|
|
44
|
-
version = "3.0.
|
|
44
|
+
version = "3.0.1"
|
|
45
45
|
if os.path.exists(manifest_path):
|
|
46
46
|
try:
|
|
47
47
|
import json
|
|
@@ -541,14 +541,14 @@ def cmd_release(args):
|
|
|
541
541
|
|
|
542
542
|
def _hub_version():
|
|
543
543
|
manifest_path = os.path.join(REPO_ROOT, "skills_manifest.json")
|
|
544
|
-
version = "3.0.
|
|
544
|
+
version = "3.0.1"
|
|
545
545
|
if os.path.exists(manifest_path):
|
|
546
546
|
try:
|
|
547
547
|
import json
|
|
548
548
|
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
549
549
|
version = json.load(f).get("version", version)
|
|
550
550
|
except (OSError, json.JSONDecodeError):
|
|
551
|
-
version = "3.0.
|
|
551
|
+
version = "3.0.1"
|
|
552
552
|
return version
|
|
553
553
|
|
|
554
554
|
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
Swarm Data Contract Validator (Single Front-Door Integrity Guard)
|
|
8
|
+
-----------------------------------------------------------------
|
|
9
|
+
Validates the JSON Data Contract emitted by `live-explorer` before
|
|
10
|
+
`pom-crafter` consumes it for Clean POM code synthesis.
|
|
11
|
+
Ensures zero locator guessing, validates semantic ARIA roles, and enforces
|
|
12
|
+
latency measurement (< 200ms) for durable selectors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
import argparse
|
|
19
|
+
from typing import Dict, Any, List, Tuple
|
|
20
|
+
|
|
21
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
22
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
23
|
+
try:
|
|
24
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
25
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
CONTRACT_SCHEMA = {
|
|
30
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
31
|
+
"title": "SelectorContract",
|
|
32
|
+
"type": "object",
|
|
33
|
+
"required": ["url", "screen_name", "elements"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"url": {"type": "string"},
|
|
36
|
+
"screen_name": {"type": "string"},
|
|
37
|
+
"viewport": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"width": {"type": "integer"},
|
|
41
|
+
"height": {"type": "integer"}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"elements": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"minItems": 1,
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["name", "role"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"name": {"type": "string"},
|
|
52
|
+
"role": {"type": "string"},
|
|
53
|
+
"selector": {"type": "string"},
|
|
54
|
+
"target": {"type": "string"},
|
|
55
|
+
"latency_ms": {"type": "number"},
|
|
56
|
+
"action_type": {"type": "string"}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_contract_dict(data: Dict[str, Any]) -> Tuple[bool, List[str]]:
|
|
65
|
+
"""Validates contract structure without external dependencies."""
|
|
66
|
+
errors = []
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
return False, ["Root contract must be a JSON object."]
|
|
69
|
+
|
|
70
|
+
# Required top-level fields
|
|
71
|
+
for field in ("url", "screen_name", "elements"):
|
|
72
|
+
if field not in data:
|
|
73
|
+
errors.append(f"Missing required top-level field: '{field}'")
|
|
74
|
+
|
|
75
|
+
if "elements" in data:
|
|
76
|
+
elements = data["elements"]
|
|
77
|
+
if not isinstance(elements, list) or len(elements) == 0:
|
|
78
|
+
errors.append("'elements' must be a non-empty list of interactive components.")
|
|
79
|
+
else:
|
|
80
|
+
for idx, el in enumerate(elements):
|
|
81
|
+
if not isinstance(el, dict):
|
|
82
|
+
errors.append(f"Element at index {idx} must be a dictionary object.")
|
|
83
|
+
continue
|
|
84
|
+
if "name" not in el or not el["name"]:
|
|
85
|
+
errors.append(f"Element at index {idx} missing 'name' identifier.")
|
|
86
|
+
if "role" not in el or not el["role"]:
|
|
87
|
+
errors.append(f"Element at index {idx} missing ARIA 'role' classification.")
|
|
88
|
+
if "selector" not in el and "target" not in el:
|
|
89
|
+
errors.append(f"Element '{el.get('name', f'index_{idx}')}' must provide 'selector' or 'target'.")
|
|
90
|
+
|
|
91
|
+
latency = el.get("latency_ms")
|
|
92
|
+
if latency is not None and isinstance(latency, (int, float)):
|
|
93
|
+
if latency > 200:
|
|
94
|
+
errors.append(
|
|
95
|
+
f"Element '{el.get('name')}' latency ({latency}ms) exceeds the 200ms latency budget."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return len(errors) == 0, errors
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_contract_file(file_path: str) -> Tuple[bool, List[str]]:
|
|
102
|
+
"""Loads and validates a contract file from disk."""
|
|
103
|
+
if not os.path.exists(file_path):
|
|
104
|
+
return False, [f"Contract file does not exist: {file_path}"]
|
|
105
|
+
try:
|
|
106
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
107
|
+
data = json.load(f)
|
|
108
|
+
return validate_contract_dict(data)
|
|
109
|
+
except json.JSONDecodeError as err:
|
|
110
|
+
return False, [f"Malformed JSON syntax: {err}"]
|
|
111
|
+
except OSError as err:
|
|
112
|
+
return False, [f"File read error: {err}"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
parser = argparse.ArgumentParser(description="Swarm Data Contract Validator")
|
|
117
|
+
parser.add_argument("contract_path", nargs="?", default=None, help="Path to JSON Data Contract file")
|
|
118
|
+
parser.add_argument("--schema", action="store_true", help="Print standard JSON schema and exit")
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
if args.schema:
|
|
122
|
+
print(json.dumps(CONTRACT_SCHEMA, indent=2))
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
if not args.contract_path:
|
|
126
|
+
parser.print_help()
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
valid, errors = validate_contract_file(args.contract_path)
|
|
130
|
+
if valid:
|
|
131
|
+
print(f"✅ [VALID CONTRACT] '{args.contract_path}' is ready for Clean POM synthesis.")
|
|
132
|
+
return 0
|
|
133
|
+
else:
|
|
134
|
+
print(f"❌ [INVALID CONTRACT] '{args.contract_path}' contains {len(errors)} violation(s):")
|
|
135
|
+
for err in errors:
|
|
136
|
+
print(f" • {err}")
|
|
137
|
+
return 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
sys.exit(main())
|
|
@@ -655,7 +655,7 @@ def build_swarm_timeline_html(default_lang: str = "vi") -> str:
|
|
|
655
655
|
("Tool Exec", "badge-tool", "pom-crafter", "synthesize_page_class (Clean POM)", "210ms", "1,850 tok", "✓ ZERO XPATHS", "badge-pass", "gen_ai.system: google | model: gemini-flash | target: PageClass"),
|
|
656
656
|
("Test Run", "badge-ver", "test-runner", "dispatch_headless_cli & distill_log", "3,200ms", "120 tok", "✓ PASS (0 Fails)", "badge-pass", "tool.name: distill_log.py | noise_reduction: 95.2%"),
|
|
657
657
|
("Memory Cache", "badge-cat", "failure-analyst", "cache_resolver.py fingerprint & lookup", "8ms", "0 tok", "⚡ 0-TOKEN RESOLVE", "badge-pass", "tool.name: cache_resolver.py | fingerprint: fp_7f2e76cf | hits: 0"),
|
|
658
|
-
("Gatekeeper", "badge-gate", "quality-gatekeeper", "verify_all.py (
|
|
658
|
+
("Gatekeeper", "badge-gate", "quality-gatekeeper", "verify_all.py (10 Gates + MicroMutator)", "1,450ms", "0 tok", "🛡️ 10/10 GATES PASS", "badge-pass", "tool.name: verify_all.py | mutation_score: 91.4% (DoD >= 80%)"),
|
|
659
659
|
]
|
|
660
660
|
|
|
661
661
|
step_rows = []
|
|
@@ -1320,7 +1320,7 @@ def _build_target_translations(
|
|
|
1320
1320
|
"th_step_status": "Trạng Thái",
|
|
1321
1321
|
"th_step_details": "Chi Tiết Kỹ Thuật (OTel & AST)",
|
|
1322
1322
|
|
|
1323
|
-
"footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.
|
|
1323
|
+
"footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.1 • Bản quyền © Thế Vị (@TeeVee998) • Chuẩn Clean POM & Kiểm Thử Tự Chủ",
|
|
1324
1324
|
}
|
|
1325
1325
|
|
|
1326
1326
|
translations_en = {
|
|
@@ -1398,7 +1398,7 @@ def _build_target_translations(
|
|
|
1398
1398
|
"th_step_status": "Status",
|
|
1399
1399
|
"th_step_details": "Technical Details (OTel & AST)",
|
|
1400
1400
|
|
|
1401
|
-
"footer_text": "Generated automatically by Synapse-Hub v3.0.
|
|
1401
|
+
"footer_text": "Generated automatically by Synapse-Hub v3.0.1 • Copyright © Thế Vị (@TeeVee998) • Clean POM & Autonomous Testing",
|
|
1402
1402
|
}
|
|
1403
1403
|
|
|
1404
1404
|
_inject_gate_translations(translations_vi, translations_en, gates)
|
|
@@ -1804,13 +1804,13 @@ def _build_hub_translations(
|
|
|
1804
1804
|
"""Builds bilingual dictionaries for Hub Dashboard."""
|
|
1805
1805
|
translations_vi = {
|
|
1806
1806
|
"header_title": "Bảng Điều Hành Chất Lượng Doanh Nghiệp",
|
|
1807
|
-
"header_subtitle": "Synapse-Hub v3.0.
|
|
1807
|
+
"header_subtitle": "Synapse-Hub v3.0.1 • Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
|
|
1808
1808
|
"nav_welcome": "Giới Thiệu",
|
|
1809
1809
|
"meta_mode": "🏷️ Chế độ: <strong>HỆ SINH THÁI QA HUB</strong>",
|
|
1810
1810
|
"meta_gen": f"🕒 Khởi tạo: <strong>{now_str}</strong>",
|
|
1811
|
-
"meta_status": "🛡️ Gatekeeper: <strong>
|
|
1811
|
+
"meta_status": "🛡️ Gatekeeper: <strong>10/10 ĐẠT CHUẨN</strong>",
|
|
1812
1812
|
|
|
1813
|
-
"kpi_dod_lbl": "
|
|
1813
|
+
"kpi_dod_lbl": "10 Cổng Kiểm Soát DoD",
|
|
1814
1814
|
"kpi_dod_sub": "✓ 100% Cổng Đạt Chuẩn",
|
|
1815
1815
|
"kpi_skills_lbl": "Kỹ Năng AI Chuyên Biệt",
|
|
1816
1816
|
"kpi_skills_sub": "Phân Bổ Trong 4 Danh Mục Doanh Nghiệp",
|
|
@@ -1874,18 +1874,18 @@ def _build_hub_translations(
|
|
|
1874
1874
|
"th_sk_ver": "Phiên Bản",
|
|
1875
1875
|
"th_sk_cap": "Khả Năng Nghiệp Vụ Cốt Lõi",
|
|
1876
1876
|
|
|
1877
|
-
"footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp • Synapse-Hub v3.0.
|
|
1877
|
+
"footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp • Synapse-Hub v3.0.1 • Bản quyền © Thế Vị (@TeeVee998) • Hệ Thống Kiểm Thử AI Agent Tự Chủ",
|
|
1878
1878
|
}
|
|
1879
1879
|
|
|
1880
1880
|
translations_en = {
|
|
1881
1881
|
"header_title": "Enterprise QA Executive Dashboard",
|
|
1882
|
-
"header_subtitle": "Synapse-Hub v3.0.
|
|
1882
|
+
"header_subtitle": "Synapse-Hub v3.0.1 • Autonomous AI Agent Testing Ecosystem",
|
|
1883
1883
|
"nav_welcome": "Welcome",
|
|
1884
1884
|
"meta_mode": "🏷️ Mode: <strong>QA HUB ECOSYSTEM</strong>",
|
|
1885
1885
|
"meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
|
|
1886
|
-
"meta_status": "🛡️ Gatekeeper: <strong>
|
|
1886
|
+
"meta_status": "🛡️ Gatekeeper: <strong>10/10 COMPLIANT</strong>",
|
|
1887
1887
|
|
|
1888
|
-
"kpi_dod_lbl": "
|
|
1888
|
+
"kpi_dod_lbl": "10 Quality Gates",
|
|
1889
1889
|
"kpi_dod_sub": "✓ 100% Verification Compliant",
|
|
1890
1890
|
"kpi_skills_lbl": "Specialized AI Skills",
|
|
1891
1891
|
"kpi_skills_sub": "Spanned across 4 Enterprise Categories",
|
|
@@ -1949,7 +1949,7 @@ def _build_hub_translations(
|
|
|
1949
1949
|
"th_sk_ver": "Version",
|
|
1950
1950
|
"th_sk_cap": "Core Capabilities",
|
|
1951
1951
|
|
|
1952
|
-
"footer_text": "Enterprise QA Executive Dashboard • Synapse-Hub v3.0.
|
|
1952
|
+
"footer_text": "Enterprise QA Executive Dashboard • Synapse-Hub v3.0.1 • Copyright © Thế Vị (@TeeVee998) • Autonomous AI Agent Testing Ecosystem",
|
|
1953
1953
|
}
|
|
1954
1954
|
|
|
1955
1955
|
_inject_gate_translations(translations_vi, translations_en, gates)
|
package/tools/db_sandbox.py
CHANGED
|
@@ -191,23 +191,36 @@ class DBSandbox:
|
|
|
191
191
|
cursor = conn.cursor()
|
|
192
192
|
executed_count = 0
|
|
193
193
|
|
|
194
|
+
safe_sp = re.sub(r'[^a-zA-Z0-9_]', '', savepoint_name) or "sp_test_run"
|
|
194
195
|
try:
|
|
195
|
-
cursor.execute(f"SAVEPOINT {
|
|
196
|
+
cursor.execute(f"SAVEPOINT {safe_sp};")
|
|
196
197
|
for sql in sql_commands:
|
|
197
198
|
cursor.execute(sql)
|
|
198
199
|
executed_count += 1
|
|
199
200
|
|
|
200
|
-
#
|
|
201
|
-
cursor.execute("SELECT
|
|
202
|
-
|
|
201
|
+
# Determine sample table for verification read (prefer 'users', fallback to first user table)
|
|
202
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
|
203
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
204
|
+
target_table = "users" if "users" in tables else (tables[0] if tables else None)
|
|
205
|
+
|
|
206
|
+
count_inside_tx = 0
|
|
207
|
+
if target_table:
|
|
208
|
+
safe_tbl = target_table.replace('"', '""')
|
|
209
|
+
cursor.execute(f'SELECT COUNT(*) FROM "{safe_tbl}"')
|
|
210
|
+
count_inside_tx = cursor.fetchone()[0]
|
|
211
|
+
else:
|
|
212
|
+
count_inside_tx = executed_count
|
|
203
213
|
|
|
204
214
|
# Enforce automatic zero-pollution rollback
|
|
205
|
-
cursor.execute(f"ROLLBACK TO SAVEPOINT {
|
|
206
|
-
cursor.execute(f"RELEASE SAVEPOINT {
|
|
215
|
+
cursor.execute(f"ROLLBACK TO SAVEPOINT {safe_sp};")
|
|
216
|
+
cursor.execute(f"RELEASE SAVEPOINT {safe_sp};")
|
|
207
217
|
|
|
208
218
|
# Verify that outside the savepoint, table is pristine
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
count_after_rollback = 0
|
|
220
|
+
if target_table:
|
|
221
|
+
safe_tbl = target_table.replace('"', '""')
|
|
222
|
+
cursor.execute(f'SELECT COUNT(*) FROM "{safe_tbl}"')
|
|
223
|
+
count_after_rollback = cursor.fetchone()[0]
|
|
211
224
|
|
|
212
225
|
return {
|
|
213
226
|
"status": "SUCCESS",
|
|
@@ -219,7 +232,7 @@ class DBSandbox:
|
|
|
219
232
|
}
|
|
220
233
|
except Exception as e:
|
|
221
234
|
try:
|
|
222
|
-
cursor.execute(f"ROLLBACK TO SAVEPOINT {
|
|
235
|
+
cursor.execute(f"ROLLBACK TO SAVEPOINT {safe_sp};")
|
|
223
236
|
except sqlite3.Error as rollback_err:
|
|
224
237
|
sys.stderr.write(f"Notice: Savepoint rollback notice: {rollback_err}\n")
|
|
225
238
|
return {
|
|
@@ -41,7 +41,7 @@ def generate_allure_report(results_dir=None, report_dir=None, clean=True, open_b
|
|
|
41
41
|
|
|
42
42
|
print(f"[+] Generating Allure HTML report from '{results_path}' to '{report_path}'...")
|
|
43
43
|
try:
|
|
44
|
-
subprocess.run(cmd, check=True)
|
|
44
|
+
subprocess.run(cmd, check=True, timeout=120.0)
|
|
45
45
|
print(f"[+] Report generated successfully at: {report_path}")
|
|
46
46
|
except subprocess.CalledProcessError as e:
|
|
47
47
|
print(f"[ERROR] Allure report generation failed with exit code {e.returncode}")
|
package/tools/infer_linter.py
CHANGED
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
import re
|
|
17
17
|
import json
|
|
18
18
|
import glob
|
|
19
|
+
import ast
|
|
19
20
|
import html as html_lib
|
|
20
21
|
import argparse
|
|
21
22
|
from datetime import datetime
|
|
@@ -76,15 +77,19 @@ def parse_with_tree_sitter(file_path, content):
|
|
|
76
77
|
return None
|
|
77
78
|
|
|
78
79
|
# Generic Detection regexes (including Cypress .should and .and assertions)
|
|
80
|
+
# Note: Standalone expect(...) without chained matcher is caught as DANGLING_ASSERTION_NO_MATCHER.
|
|
79
81
|
ASSERTION_PATTERN = re.compile(
|
|
80
|
-
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b
|
|
82
|
+
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b'
|
|
83
|
+
r'|\.should\s*\('
|
|
84
|
+
r'|\.and\s*\(',
|
|
85
|
+
re.MULTILINE
|
|
81
86
|
)
|
|
82
87
|
|
|
83
88
|
# Rule L1 & L5: Empty catch / swallowed exceptions across JS/TS/Groovy/Java and Python
|
|
84
|
-
# Also catches try/catch that only console.log or promise .catch(() => {})
|
|
89
|
+
# Also catches try/catch that only console.log/logger.error or promise .catch(() => {})
|
|
85
90
|
EMPTY_CATCH_PATTERN = re.compile(
|
|
86
|
-
r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}'
|
|
87
|
-
r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}\s*\)'
|
|
91
|
+
r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}'
|
|
92
|
+
r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}\s*\)'
|
|
88
93
|
r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
|
|
89
94
|
re.MULTILINE
|
|
90
95
|
)
|
|
@@ -96,16 +101,48 @@ BENIGN_TRY_CALL_PATTERN = re.compile(
|
|
|
96
101
|
r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
|
|
97
102
|
)
|
|
98
103
|
|
|
99
|
-
# Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r,
|
|
104
|
+
# Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, assert f() == f(), assert len(x) >= 0)
|
|
100
105
|
TAUTOLOGY_PATTERN = re.compile(
|
|
101
|
-
r'(?:expect
|
|
106
|
+
r'(?:expect(?:\.soft|\.poll)?\s*\(\s*(?:true|1|null|undefined)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy|toBeNull|toBeUndefined)\s*\(\s*(?:true|1|null|undefined)?\s*\)'
|
|
107
|
+
r'|expect(?:\.soft|\.poll)?\s*\(\s*([a-zA-Z0-9_]+)\s*\)\s*\.\s*(?:toBe|toEqual)\s*\(\s*\1\s*\)'
|
|
102
108
|
r'|\bassert\s+(?:True|true|1\s*==\s*1)\b'
|
|
103
|
-
r'|\bassert\s+([a-zA-Z0-9_]+)\s*==\s*\1\b'
|
|
104
|
-
r'
|
|
109
|
+
r'|\bassert\s+([a-zA-Z0-9_\.]+(?:\([^)]*\))?)\s*==\s*\1\b'
|
|
110
|
+
r'|\bassert\s+(?:len\s*\([^)]*\)|[a-zA-Z0-9_\.]+\.(?:length|size\(\)))\s*(?:>=\s*0|>\s*-(?:1|0))\b'
|
|
111
|
+
r'|assertEquals\s*\(\s*(?:true|1|null)\s*,\s*(?:true|1|null)\s*\)'
|
|
105
112
|
r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
|
|
106
113
|
re.IGNORECASE
|
|
107
114
|
)
|
|
108
115
|
|
|
116
|
+
|
|
117
|
+
def find_dangling_expects(clean_code, raw_lines):
|
|
118
|
+
"""
|
|
119
|
+
Detects dangling expect(...) calls lacking chained matchers (e.g. expect(page) without .toBeVisible()).
|
|
120
|
+
Correctly handles nested parentheses like expect(page.locator('#id')).
|
|
121
|
+
"""
|
|
122
|
+
dangling = []
|
|
123
|
+
pattern = re.compile(r'(?:await\s+)?\bexpect(?:\.soft|\.poll)?\s*\(')
|
|
124
|
+
for m in pattern.finditer(clean_code):
|
|
125
|
+
start_idx = m.end()
|
|
126
|
+
depth = 1
|
|
127
|
+
pos = start_idx
|
|
128
|
+
while pos < len(clean_code) and depth > 0:
|
|
129
|
+
ch = clean_code[pos]
|
|
130
|
+
if ch == '(':
|
|
131
|
+
depth += 1
|
|
132
|
+
elif ch == ')':
|
|
133
|
+
depth -= 1
|
|
134
|
+
elif ch in (';', '\n') and depth > 0:
|
|
135
|
+
# Malformed unclosed expect
|
|
136
|
+
break
|
|
137
|
+
pos += 1
|
|
138
|
+
if depth == 0:
|
|
139
|
+
rest = clean_code[pos:].lstrip()
|
|
140
|
+
if not rest.startswith('.'):
|
|
141
|
+
line_no = clean_code[:m.start()].count("\n") + 1
|
|
142
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
143
|
+
dangling.append((line_no, raw_line))
|
|
144
|
+
return dangling
|
|
145
|
+
|
|
109
146
|
# Rule L4: Unawaited Playwright assertions in JS/TS
|
|
110
147
|
# Expanded with all standard Playwright async matchers and optional .not modifier
|
|
111
148
|
PLAYWRIGHT_ASYNC_MATCHERS = (
|
|
@@ -300,7 +337,7 @@ def format_junit(report_data):
|
|
|
300
337
|
return "\n".join(lines)
|
|
301
338
|
|
|
302
339
|
|
|
303
|
-
def classify_file_role(file_path: str) -> str:
|
|
340
|
+
def classify_file_role(file_path: str, content: str = "") -> str:
|
|
304
341
|
"""
|
|
305
342
|
Classifies a source file into:
|
|
306
343
|
- 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
|
|
@@ -324,6 +361,8 @@ def classify_file_role(file_path: str) -> str:
|
|
|
324
361
|
return "PAGE_OBJECT"
|
|
325
362
|
if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
|
|
326
363
|
return "PAGE_OBJECT"
|
|
364
|
+
if content and (re.search(r'\bclass\s+[A-Za-z0-9_]*Page\b', content) or re.search(r'\bextends\s+BasePage\b', content)):
|
|
365
|
+
return "PAGE_OBJECT"
|
|
327
366
|
|
|
328
367
|
# 3. Helpers & Fixtures / Tools & Utilities / Benchmarks
|
|
329
368
|
if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin", "benchmarks", "benchmark") for p in parts):
|
|
@@ -348,13 +387,16 @@ def classify_file_role(file_path: str) -> str:
|
|
|
348
387
|
|
|
349
388
|
def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
350
389
|
"""Performs static rule scanning on a single test script or automation asset."""
|
|
351
|
-
role = classify_file_role(file_path)
|
|
352
|
-
|
|
353
390
|
try:
|
|
354
391
|
rel_path = os.path.relpath(file_path, REPO_ROOT)
|
|
355
392
|
except ValueError:
|
|
356
393
|
rel_path = os.path.abspath(file_path)
|
|
357
394
|
|
|
395
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
396
|
+
content = f.read()
|
|
397
|
+
|
|
398
|
+
role = classify_file_role(file_path, content=content)
|
|
399
|
+
|
|
358
400
|
# Config files have zero test assertions by definition
|
|
359
401
|
if role == "CONFIG":
|
|
360
402
|
return {
|
|
@@ -364,9 +406,6 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
364
406
|
"is_clean": True
|
|
365
407
|
}
|
|
366
408
|
|
|
367
|
-
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
368
|
-
content = f.read()
|
|
369
|
-
|
|
370
409
|
defects = []
|
|
371
410
|
|
|
372
411
|
# Lexically strip comments and string literals for accurate analysis
|
|
@@ -402,12 +441,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
402
441
|
elif arg.isdigit():
|
|
403
442
|
delay_val = int(arg)
|
|
404
443
|
if delay_val > 0:
|
|
405
|
-
|
|
444
|
+
is_excessive = delay_val >= 3000
|
|
445
|
+
d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
|
|
446
|
+
sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
|
|
406
447
|
defects.append({
|
|
407
448
|
"type": d_type,
|
|
408
|
-
"severity":
|
|
449
|
+
"severity": sev,
|
|
409
450
|
"line": idx,
|
|
410
|
-
"message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
451
|
+
"message": f"Static sleep of {delay_val}ms found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
411
452
|
})
|
|
412
453
|
continue
|
|
413
454
|
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
|
|
@@ -428,12 +469,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
428
469
|
if delay_val > 0:
|
|
429
470
|
delay_ms = int(delay_val * 1000)
|
|
430
471
|
delay_str = f"{delay_val:g}"
|
|
431
|
-
|
|
472
|
+
is_excessive = delay_ms >= 3000
|
|
473
|
+
d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
|
|
474
|
+
sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
|
|
432
475
|
defects.append({
|
|
433
476
|
"type": d_type,
|
|
434
|
-
"severity":
|
|
477
|
+
"severity": sev,
|
|
435
478
|
"line": idx,
|
|
436
|
-
"message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
479
|
+
"message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
437
480
|
})
|
|
438
481
|
except ValueError:
|
|
439
482
|
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
|
|
@@ -470,6 +513,68 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
470
513
|
"message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
|
|
471
514
|
})
|
|
472
515
|
|
|
516
|
+
# Rule 4.1: Dangling expect(...) calls without chained matcher
|
|
517
|
+
for d_line, d_raw in find_dangling_expects(clean_code, raw_lines):
|
|
518
|
+
defects.append({
|
|
519
|
+
"type": "DANGLING_ASSERTION_NO_MATCHER",
|
|
520
|
+
"severity": "CRITICAL",
|
|
521
|
+
"line": d_line,
|
|
522
|
+
"message": f"Dangling expect() call without matcher detected ('{d_raw}'). expect() returns an expectation object and does not perform any assertion without a matcher (e.g. .toBeVisible(), .toHaveText())."
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
# Rule 4.2: Python AST Compiler-Grade Verification for .py Test Scripts
|
|
526
|
+
if file_path.endswith(".py"):
|
|
527
|
+
try:
|
|
528
|
+
tree = ast.parse(content, filename=file_path)
|
|
529
|
+
for node in ast.walk(tree):
|
|
530
|
+
if isinstance(node, ast.Assert):
|
|
531
|
+
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1:
|
|
532
|
+
op = node.test.ops[0]
|
|
533
|
+
right = node.test.comparators[0]
|
|
534
|
+
if isinstance(op, ast.Eq):
|
|
535
|
+
if ast.dump(node.test.left) == ast.dump(right):
|
|
536
|
+
line_no = getattr(node, "lineno", 1)
|
|
537
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
538
|
+
defects.append({
|
|
539
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
540
|
+
"severity": "CRITICAL",
|
|
541
|
+
"line": line_no,
|
|
542
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). Both sides of comparison evaluate to identical AST expressions."
|
|
543
|
+
})
|
|
544
|
+
elif isinstance(op, (ast.GtE, ast.Gt)):
|
|
545
|
+
if isinstance(node.test.left, ast.Call) and getattr(node.test.left.func, 'id', None) == 'len':
|
|
546
|
+
if isinstance(right, ast.Constant):
|
|
547
|
+
if (isinstance(op, ast.GtE) and right.value == 0) or (isinstance(op, ast.Gt) and right.value < 0):
|
|
548
|
+
line_no = getattr(node, "lineno", 1)
|
|
549
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
550
|
+
defects.append({
|
|
551
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
552
|
+
"severity": "CRITICAL",
|
|
553
|
+
"line": line_no,
|
|
554
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). len() is non-negative by definition."
|
|
555
|
+
})
|
|
556
|
+
elif isinstance(node.test, ast.Constant) and node.test.value is True:
|
|
557
|
+
line_no = getattr(node, "lineno", 1)
|
|
558
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
559
|
+
defects.append({
|
|
560
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
561
|
+
"severity": "CRITICAL",
|
|
562
|
+
"line": line_no,
|
|
563
|
+
"message": f"Tautological assertion detected via AST ('{raw_line}'). Constant True assertion."
|
|
564
|
+
})
|
|
565
|
+
elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
566
|
+
if getattr(node.value.func, 'id', None) == 'expect':
|
|
567
|
+
line_no = getattr(node, "lineno", 1)
|
|
568
|
+
raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
569
|
+
defects.append({
|
|
570
|
+
"type": "DANGLING_ASSERTION_NO_MATCHER",
|
|
571
|
+
"severity": "CRITICAL",
|
|
572
|
+
"line": line_no,
|
|
573
|
+
"message": f"Dangling expect() call without matcher detected via AST ('{raw_line}')."
|
|
574
|
+
})
|
|
575
|
+
except SyntaxError:
|
|
576
|
+
_ = False # synapse-ignore
|
|
577
|
+
|
|
473
578
|
# Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
|
|
474
579
|
if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
|
|
475
580
|
for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
|
|
@@ -544,6 +649,15 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
|
544
649
|
"message": "Test case file contains executable logic but has no verification or assertion statements."
|
|
545
650
|
})
|
|
546
651
|
|
|
652
|
+
seen_keys = set()
|
|
653
|
+
deduped = []
|
|
654
|
+
for d in defects:
|
|
655
|
+
k = (d.get("type"), d.get("line"))
|
|
656
|
+
if k not in seen_keys:
|
|
657
|
+
seen_keys.add(k)
|
|
658
|
+
deduped.append(d)
|
|
659
|
+
defects = deduped
|
|
660
|
+
|
|
547
661
|
active_defects = []
|
|
548
662
|
disabled = {r.upper() for r in (disabled_rules or [])}
|
|
549
663
|
norm_rel = rel_path.replace("\\", "/")
|
|
@@ -777,7 +891,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
|
|
|
777
891
|
sys.stderr.write(f"Notice: Target report write failed: {err}\n")
|
|
778
892
|
|
|
779
893
|
if strict:
|
|
780
|
-
fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST"))
|
|
894
|
+
fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST", "DANGLING_ASSERTION_NO_MATCHER"))
|
|
781
895
|
return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
|
|
782
896
|
return 0 if critical_count == 0 else 1
|
|
783
897
|
|
|
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
23
23
|
except Exception:
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
|
-
SERVER_VERSION = "3.0.
|
|
26
|
+
SERVER_VERSION = "3.0.1"
|
|
27
27
|
|
|
28
28
|
NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
|
|
29
29
|
NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
|