@lugom.io/hefesto 0.1.2 → 0.3.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/hefesto-argos.md +279 -0
- package/agents/hefesto-athena.md +379 -0
- package/agents/hefesto-hermes.md +128 -0
- package/bin/install.js +97 -20
- package/package.json +3 -3
- package/skills/hefesto-context/SKILL.md +38 -6
- package/skills/hefesto-design/SKILL.md +194 -0
- package/skills/hefesto-design/data/animations.csv +21 -0
- package/skills/hefesto-design/data/anti-patterns.csv +41 -0
- package/skills/hefesto-design/data/charts.csv +26 -0
- package/skills/hefesto-design/data/colors.csv +108 -0
- package/skills/hefesto-design/data/components.csv +31 -0
- package/skills/hefesto-design/data/google-fonts.csv +56 -0
- package/skills/hefesto-design/data/icons.csv +23 -0
- package/skills/hefesto-design/data/landing-pages.csv +28 -0
- package/skills/hefesto-design/data/products.csv +46 -0
- package/skills/hefesto-design/data/spacing.csv +16 -0
- package/skills/hefesto-design/data/styles.csv +53 -0
- package/skills/hefesto-design/data/typography.csv +41 -0
- package/skills/hefesto-design/data/ux-rules.csv +61 -0
- package/skills/hefesto-design/references/accessibility.md +335 -0
- package/skills/hefesto-design/references/aesthetics.md +343 -0
- package/skills/hefesto-design/references/anti-patterns.md +107 -0
- package/skills/hefesto-design/references/checklist.md +66 -0
- package/skills/hefesto-design/references/color-psychology.md +203 -0
- package/skills/hefesto-design/references/component-specs.md +318 -0
- package/skills/hefesto-design/references/polish.md +339 -0
- package/skills/hefesto-design/references/token-architecture.md +394 -0
- package/skills/hefesto-design/references/ux-rules.md +349 -0
- package/skills/hefesto-design/scripts/__pycache__/audit.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/__pycache__/contrast.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/__pycache__/search.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/__pycache__/validate_tokens.cpython-314.pyc +0 -0
- package/skills/hefesto-design/scripts/audit.py +450 -0
- package/skills/hefesto-design/scripts/contrast.py +195 -0
- package/skills/hefesto-design/scripts/core.py +155 -0
- package/skills/hefesto-design/scripts/design_system.py +311 -0
- package/skills/hefesto-design/scripts/search.py +235 -0
- package/skills/hefesto-design/scripts/validate_tokens.py +274 -0
- package/{commands/hefesto/init.md → skills/hefesto-init/SKILL.md} +28 -3
- package/{commands/hefesto/new-feature.md → skills/hefesto-new-feature/SKILL.md} +5 -2
- package/{commands/hefesto/update.md → skills/hefesto-update/SKILL.md} +6 -3
- package/templates/DESIGN.md +137 -0
- package/templates/RECON.md +54 -0
- package/templates/RESEARCH.md +87 -0
- package/templates/STATE.md +1 -1
- package/templates/VERDICT.md +52 -0
- package/agents/.gitkeep +0 -0
- package/commands/hefesto/status.md +0 -40
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI de busca multi-domínio para o sistema de design do Hefesto.
|
|
3
|
+
|
|
4
|
+
Busca em CSVs de estilos, cores, tipografia, componentes, UX, anti-patterns,
|
|
5
|
+
espaçamento e animações usando BM25.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Resolve import do core.py no mesmo diretório
|
|
14
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
15
|
+
sys.path.insert(0, str(_SCRIPT_DIR))
|
|
16
|
+
|
|
17
|
+
from core import BM25
|
|
18
|
+
|
|
19
|
+
DATA_DIR = _SCRIPT_DIR.parent / "data"
|
|
20
|
+
|
|
21
|
+
DOMAINS = {
|
|
22
|
+
"style": {
|
|
23
|
+
"csv": "styles.csv",
|
|
24
|
+
"fields": ["name", "keywords_pt", "keywords_en", "description", "mood", "best_for"],
|
|
25
|
+
"label": "Estilos",
|
|
26
|
+
},
|
|
27
|
+
"color": {
|
|
28
|
+
"csv": "colors.csv",
|
|
29
|
+
"fields": ["name", "industry", "mood", "description"],
|
|
30
|
+
"label": "Paletas de Cores",
|
|
31
|
+
},
|
|
32
|
+
"typography": {
|
|
33
|
+
"csv": "typography.csv",
|
|
34
|
+
"fields": ["name", "mood", "best_for", "display_font", "body_font", "pairing_reason"],
|
|
35
|
+
"label": "Tipografia",
|
|
36
|
+
},
|
|
37
|
+
"component": {
|
|
38
|
+
"csv": "components.csv",
|
|
39
|
+
"fields": ["name", "category", "variants", "description"],
|
|
40
|
+
"label": "Componentes",
|
|
41
|
+
},
|
|
42
|
+
"ux": {
|
|
43
|
+
"csv": "ux-rules.csv",
|
|
44
|
+
"fields": ["name", "category", "rule", "standard", "reasoning"],
|
|
45
|
+
"label": "Regras de UX",
|
|
46
|
+
},
|
|
47
|
+
"anti-pattern": {
|
|
48
|
+
"csv": "anti-patterns.csv",
|
|
49
|
+
"fields": ["name", "category", "description", "why_bad"],
|
|
50
|
+
"label": "Anti-Patterns",
|
|
51
|
+
},
|
|
52
|
+
"spacing": {
|
|
53
|
+
"csv": "spacing.csv",
|
|
54
|
+
"fields": ["name", "style", "description", "density"],
|
|
55
|
+
"label": "Espaçamento",
|
|
56
|
+
},
|
|
57
|
+
"animation": {
|
|
58
|
+
"csv": "animations.csv",
|
|
59
|
+
"fields": ["name", "context", "description"],
|
|
60
|
+
"label": "Animações",
|
|
61
|
+
},
|
|
62
|
+
"product": {
|
|
63
|
+
"csv": "products.csv",
|
|
64
|
+
"fields": ["name", "category", "audience", "recommended_style", "key_components", "layout_pattern", "description"],
|
|
65
|
+
"label": "Tipos de Produto",
|
|
66
|
+
},
|
|
67
|
+
"chart": {
|
|
68
|
+
"csv": "charts.csv",
|
|
69
|
+
"fields": ["name", "category", "best_for", "data_type", "a11y_notes", "description"],
|
|
70
|
+
"label": "Gráficos e Visualização",
|
|
71
|
+
},
|
|
72
|
+
"landing": {
|
|
73
|
+
"csv": "landing-pages.csv",
|
|
74
|
+
"fields": ["name", "section_type", "purpose", "key_elements", "copywriting_pattern", "description"],
|
|
75
|
+
"label": "Landing Pages",
|
|
76
|
+
},
|
|
77
|
+
"icon": {
|
|
78
|
+
"csv": "icons.csv",
|
|
79
|
+
"fields": ["name", "style", "best_for", "avoid_with", "description"],
|
|
80
|
+
"label": "Icon Sets",
|
|
81
|
+
},
|
|
82
|
+
"font": {
|
|
83
|
+
"csv": "google-fonts.csv",
|
|
84
|
+
"fields": ["name", "category", "mood", "best_for", "pair_with", "description"],
|
|
85
|
+
"label": "Google Fonts",
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_engine(domain_key):
|
|
91
|
+
"""Cria um BM25 engine para o domínio dado."""
|
|
92
|
+
domain = DOMAINS[domain_key]
|
|
93
|
+
csv_path = DATA_DIR / domain["csv"]
|
|
94
|
+
if not csv_path.exists():
|
|
95
|
+
print(f"Erro: arquivo não encontrado: {csv_path}", file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
return BM25(str(csv_path), domain["fields"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _display_columns(results):
|
|
101
|
+
"""Determina colunas visíveis (exclui internas e muito longas)."""
|
|
102
|
+
if not results:
|
|
103
|
+
return []
|
|
104
|
+
skip = {"_score"}
|
|
105
|
+
return [k for k in results[0].keys() if k not in skip]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _format_table(results, domain_label, max_col_width=40):
|
|
109
|
+
"""Formata resultados como tabela alinhada."""
|
|
110
|
+
if not results:
|
|
111
|
+
return f" Nenhum resultado encontrado para '{domain_label}'."
|
|
112
|
+
|
|
113
|
+
cols = _display_columns(results)
|
|
114
|
+
cols_with_score = cols + ["score"]
|
|
115
|
+
|
|
116
|
+
# Calcula largura de cada coluna
|
|
117
|
+
widths = {}
|
|
118
|
+
for col in cols_with_score:
|
|
119
|
+
header_len = len(col)
|
|
120
|
+
max_val = 0
|
|
121
|
+
for r in results:
|
|
122
|
+
val = str(r.get("_score", "")) if col == "score" else str(r.get(col, ""))
|
|
123
|
+
if len(val) > max_col_width:
|
|
124
|
+
val = val[:max_col_width - 3] + "..."
|
|
125
|
+
max_val = max(max_val, len(val))
|
|
126
|
+
widths[col] = max(header_len, min(max_val, max_col_width))
|
|
127
|
+
|
|
128
|
+
# Header
|
|
129
|
+
header = " | ".join(col.ljust(widths[col]) for col in cols_with_score)
|
|
130
|
+
separator = "-+-".join("-" * widths[col] for col in cols_with_score)
|
|
131
|
+
|
|
132
|
+
lines = [f"\n === {domain_label} ===\n", f" {header}", f" {separator}"]
|
|
133
|
+
|
|
134
|
+
for r in results:
|
|
135
|
+
row_parts = []
|
|
136
|
+
for col in cols_with_score:
|
|
137
|
+
val = str(r.get("_score", "")) if col == "score" else str(r.get(col, ""))
|
|
138
|
+
if len(val) > max_col_width:
|
|
139
|
+
val = val[:max_col_width - 3] + "..."
|
|
140
|
+
row_parts.append(val.ljust(widths[col]))
|
|
141
|
+
lines.append(" " + " | ".join(row_parts))
|
|
142
|
+
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _search_domain(domain_key, query, max_results):
|
|
147
|
+
"""Busca em um domínio e retorna resultados."""
|
|
148
|
+
engine = _get_engine(domain_key)
|
|
149
|
+
return engine.search(query, max_results)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _list_domains():
|
|
153
|
+
"""Lista todos os domínios disponíveis."""
|
|
154
|
+
print("\nDomínios disponíveis:\n")
|
|
155
|
+
for key, info in DOMAINS.items():
|
|
156
|
+
csv_path = DATA_DIR / info["csv"]
|
|
157
|
+
exists = "OK" if csv_path.exists() else "FALTANDO"
|
|
158
|
+
print(f" {key:15s} {info['label']:25s} [{exists}] {info['csv']}")
|
|
159
|
+
print()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
parser = argparse.ArgumentParser(
|
|
164
|
+
description="Busca multi-domínio no sistema de design Hefesto.",
|
|
165
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
166
|
+
epilog="""Exemplos:
|
|
167
|
+
python3 search.py "fintech modern" --domain style
|
|
168
|
+
python3 search.py "luxury serif" --domain typography
|
|
169
|
+
python3 search.py "wellness calm" --all
|
|
170
|
+
python3 search.py --list-domains
|
|
171
|
+
""",
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument("query", nargs="?", default=None, help="Termos de busca")
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--domain", "-d",
|
|
176
|
+
choices=list(DOMAINS.keys()),
|
|
177
|
+
help="Domínio para buscar",
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument("--all", "-a", action="store_true", help="Buscar em todos os domínios")
|
|
180
|
+
parser.add_argument("--list-domains", action="store_true", help="Listar domínios disponíveis")
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"-f", "--format",
|
|
183
|
+
choices=["table", "json"],
|
|
184
|
+
default="table",
|
|
185
|
+
help="Formato de saída (padrão: table)",
|
|
186
|
+
)
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"-n", "--max-results",
|
|
189
|
+
type=int,
|
|
190
|
+
default=5,
|
|
191
|
+
help="Número máximo de resultados por domínio (padrão: 5)",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
|
|
196
|
+
if args.list_domains:
|
|
197
|
+
_list_domains()
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if not args.query:
|
|
201
|
+
parser.error("Informe os termos de busca ou use --list-domains.")
|
|
202
|
+
|
|
203
|
+
if not args.domain and not args.all:
|
|
204
|
+
parser.error("Informe --domain DOMÍNIO ou --all para buscar em todos.")
|
|
205
|
+
|
|
206
|
+
domains_to_search = list(DOMAINS.keys()) if args.all else [args.domain]
|
|
207
|
+
|
|
208
|
+
all_results = {}
|
|
209
|
+
for domain_key in domains_to_search:
|
|
210
|
+
try:
|
|
211
|
+
results = _search_domain(domain_key, args.query, args.max_results)
|
|
212
|
+
if results:
|
|
213
|
+
all_results[domain_key] = results
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
if not args.all:
|
|
216
|
+
print(f"Erro: CSV do domínio '{domain_key}' não encontrado.", file=sys.stderr)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
if args.format == "json":
|
|
220
|
+
output = {}
|
|
221
|
+
for domain_key, results in all_results.items():
|
|
222
|
+
output[domain_key] = results
|
|
223
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
224
|
+
else:
|
|
225
|
+
if not all_results:
|
|
226
|
+
print("\n Nenhum resultado encontrado.\n")
|
|
227
|
+
return
|
|
228
|
+
for domain_key, results in all_results.items():
|
|
229
|
+
label = DOMAINS[domain_key]["label"]
|
|
230
|
+
print(_format_table(results, label))
|
|
231
|
+
print()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validador de uso de design tokens para o Hefesto.
|
|
3
|
+
|
|
4
|
+
Verifica se tokens definidos no DESIGN.md são usados no código-fonte e
|
|
5
|
+
identifica valores hardcoded que deveriam usar tokens.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCANNABLE_EXTENSIONS = {
|
|
16
|
+
".tsx", ".jsx", ".vue", ".svelte", ".css", ".scss",
|
|
17
|
+
".html", ".astro", ".ts", ".js",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_binary(filepath):
|
|
22
|
+
"""Verifica se o arquivo parece ser binário."""
|
|
23
|
+
try:
|
|
24
|
+
with open(filepath, "rb") as f:
|
|
25
|
+
chunk = f.read(1024)
|
|
26
|
+
return b"\x00" in chunk
|
|
27
|
+
except (IOError, OSError):
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_tokens(design_path):
|
|
32
|
+
"""Extrai tokens de design da seção 'Tokens de Design' do DESIGN.md.
|
|
33
|
+
|
|
34
|
+
Retorna dict: {token_name: token_value}
|
|
35
|
+
Ex: {"--color-primary": "#6366f1", "--space-md": "16px"}
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
with open(design_path, "r", encoding="utf-8") as f:
|
|
39
|
+
content = f.read()
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
print(f"Erro: arquivo não encontrado: {design_path}", file=sys.stderr)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
tokens = {}
|
|
45
|
+
in_section = False
|
|
46
|
+
|
|
47
|
+
for line in content.split("\n"):
|
|
48
|
+
# Detecta início da seção de tokens
|
|
49
|
+
if re.match(r"^#{1,4}\s+.*[Tt]okens\s+de\s+[Dd]esign", line):
|
|
50
|
+
in_section = True
|
|
51
|
+
continue
|
|
52
|
+
# Detecta fim da seção (próximo heading de mesmo nível ou superior)
|
|
53
|
+
if in_section and re.match(r"^#{1,3}\s+", line) and "token" not in line.lower():
|
|
54
|
+
break
|
|
55
|
+
if not in_section:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Extrai custom properties CSS: --nome: valor
|
|
59
|
+
token_match = re.search(r"(--[\w-]+)\s*:\s*([^;}\n]+)", line)
|
|
60
|
+
if token_match:
|
|
61
|
+
name = token_match.group(1).strip()
|
|
62
|
+
value = token_match.group(2).strip().rstrip(";")
|
|
63
|
+
tokens[name] = value
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# Também captura tokens em formato de tabela markdown: | --nome | valor |
|
|
67
|
+
table_match = re.search(r"\|\s*(--[\w-]+)\s*\|\s*([^|]+)\s*\|", line)
|
|
68
|
+
if table_match:
|
|
69
|
+
name = table_match.group(1).strip()
|
|
70
|
+
value = table_match.group(2).strip()
|
|
71
|
+
tokens[name] = value
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Formato com backtick: `--nome` → `valor` ou `--nome`: `valor`
|
|
75
|
+
backtick_match = re.search(r"`(--[\w-]+)`\s*[:\u2192→=]\s*`?([^`\n]+)`?", line)
|
|
76
|
+
if backtick_match:
|
|
77
|
+
name = backtick_match.group(1).strip()
|
|
78
|
+
value = backtick_match.group(2).strip().rstrip("`")
|
|
79
|
+
tokens[name] = value
|
|
80
|
+
|
|
81
|
+
return tokens
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def walk_source(src_dir):
|
|
85
|
+
"""Percorre diretório fonte e retorna arquivos escaneáveis."""
|
|
86
|
+
files = []
|
|
87
|
+
for root, _dirs, filenames in os.walk(src_dir):
|
|
88
|
+
parts = root.split(os.sep)
|
|
89
|
+
if any(p in {"node_modules", ".git", "dist", "build", ".next", "__pycache__"} for p in parts):
|
|
90
|
+
continue
|
|
91
|
+
for fname in filenames:
|
|
92
|
+
ext = os.path.splitext(fname)[1].lower()
|
|
93
|
+
if ext in SCANNABLE_EXTENSIONS:
|
|
94
|
+
files.append(os.path.join(root, fname))
|
|
95
|
+
return files
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_tokens(design_path, src_dir):
|
|
99
|
+
"""Valida uso de tokens no código-fonte.
|
|
100
|
+
|
|
101
|
+
Retorna dict com:
|
|
102
|
+
tokens_defined: total de tokens definidos
|
|
103
|
+
tokens_used: set de tokens encontrados no código
|
|
104
|
+
tokens_unused: set de tokens não encontrados
|
|
105
|
+
hardcoded_matches: lista de {file, line, token_name, hardcoded_value}
|
|
106
|
+
coverage: percentual de cobertura
|
|
107
|
+
"""
|
|
108
|
+
tokens = parse_tokens(design_path)
|
|
109
|
+
if not tokens:
|
|
110
|
+
print("Aviso: nenhum token encontrado na seção 'Tokens de Design'.", file=sys.stderr)
|
|
111
|
+
return {
|
|
112
|
+
"tokens_defined": 0,
|
|
113
|
+
"tokens_used": set(),
|
|
114
|
+
"tokens_unused": set(),
|
|
115
|
+
"hardcoded_matches": [],
|
|
116
|
+
"coverage": 0.0,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
files = walk_source(src_dir)
|
|
120
|
+
tokens_used = set()
|
|
121
|
+
hardcoded_matches = []
|
|
122
|
+
|
|
123
|
+
# Pré-computa mapeamento valor → nomes de token (para detectar hardcoded)
|
|
124
|
+
value_to_tokens = {}
|
|
125
|
+
for name, value in tokens.items():
|
|
126
|
+
val_normalized = value.strip().lower()
|
|
127
|
+
if val_normalized and len(val_normalized) >= 2:
|
|
128
|
+
value_to_tokens.setdefault(val_normalized, []).append(name)
|
|
129
|
+
|
|
130
|
+
for filepath in files:
|
|
131
|
+
if _is_binary(filepath):
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
|
135
|
+
lines = f.readlines()
|
|
136
|
+
except (IOError, OSError):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
file_content = "".join(lines)
|
|
140
|
+
|
|
141
|
+
# Verifica quais tokens são usados
|
|
142
|
+
for token_name in tokens:
|
|
143
|
+
if token_name in file_content:
|
|
144
|
+
tokens_used.add(token_name)
|
|
145
|
+
|
|
146
|
+
# Busca valores hardcoded que correspondem a valores de tokens
|
|
147
|
+
for lineno, line in enumerate(lines, 1):
|
|
148
|
+
# Ignora linhas de comentário
|
|
149
|
+
stripped = line.strip()
|
|
150
|
+
if (stripped.startswith("//") or stripped.startswith("/*")
|
|
151
|
+
or stripped.startswith("*") or stripped.startswith("#")
|
|
152
|
+
or stripped.startswith("<!--")):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
line_lower = line.lower()
|
|
156
|
+
|
|
157
|
+
for val_normalized, token_names in value_to_tokens.items():
|
|
158
|
+
# Use word boundary matching to avoid substring false positives
|
|
159
|
+
# e.g., token value "16" should not match "216px"
|
|
160
|
+
# Build a pattern that matches the value with surrounding context
|
|
161
|
+
escaped_val = re.escape(val_normalized)
|
|
162
|
+
# Match value with word boundaries or common delimiters
|
|
163
|
+
boundary_pattern = re.compile(
|
|
164
|
+
r"(?:^|[\s:=,;({\[])%s(?:$|[\s;,)}\]px%%rem em])" % escaped_val,
|
|
165
|
+
re.IGNORECASE,
|
|
166
|
+
)
|
|
167
|
+
if boundary_pattern.search(line_lower):
|
|
168
|
+
# Verifica se não está dentro de var() ou é definição do próprio token
|
|
169
|
+
is_var_usage = re.search(r"var\(\s*" + re.escape(token_names[0]), line)
|
|
170
|
+
is_definition = any(tn in line for tn in token_names)
|
|
171
|
+
if not is_var_usage and not is_definition:
|
|
172
|
+
hardcoded_matches.append({
|
|
173
|
+
"file": filepath,
|
|
174
|
+
"line": lineno,
|
|
175
|
+
"token_name": ", ".join(token_names),
|
|
176
|
+
"hardcoded_value": val_normalized,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
tokens_unused = set(tokens.keys()) - tokens_used
|
|
180
|
+
total = len(tokens)
|
|
181
|
+
coverage = (len(tokens_used) / total * 100) if total > 0 else 0.0
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"tokens_defined": total,
|
|
185
|
+
"tokens_used": tokens_used,
|
|
186
|
+
"tokens_unused": tokens_unused,
|
|
187
|
+
"hardcoded_matches": hardcoded_matches,
|
|
188
|
+
"coverage": coverage,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def format_report(result, fmt="table"):
|
|
193
|
+
"""Formata e imprime relatório de validação."""
|
|
194
|
+
if fmt == "json":
|
|
195
|
+
output = {
|
|
196
|
+
"tokens_defined": result["tokens_defined"],
|
|
197
|
+
"tokens_used": sorted(result["tokens_used"]),
|
|
198
|
+
"tokens_unused": sorted(result["tokens_unused"]),
|
|
199
|
+
"hardcoded_matches": result["hardcoded_matches"],
|
|
200
|
+
"coverage": round(result["coverage"], 1),
|
|
201
|
+
}
|
|
202
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
print(f"\n{'='*50}")
|
|
206
|
+
print(f" RELATÓRIO DE TOKENS DE DESIGN")
|
|
207
|
+
print(f"{'='*50}\n")
|
|
208
|
+
|
|
209
|
+
total = result["tokens_defined"]
|
|
210
|
+
used = len(result["tokens_used"])
|
|
211
|
+
unused = len(result["tokens_unused"])
|
|
212
|
+
|
|
213
|
+
print(f" Tokens definidos: {total}")
|
|
214
|
+
print(f" Tokens utilizados: {used}")
|
|
215
|
+
print(f" Tokens não usados: {unused}")
|
|
216
|
+
print(f" Cobertura: {result['coverage']:.1f}%\n")
|
|
217
|
+
|
|
218
|
+
if result["tokens_unused"]:
|
|
219
|
+
print(" TOKENS NÃO UTILIZADOS:")
|
|
220
|
+
for token in sorted(result["tokens_unused"]):
|
|
221
|
+
print(f" - {token}")
|
|
222
|
+
print()
|
|
223
|
+
|
|
224
|
+
if result["hardcoded_matches"]:
|
|
225
|
+
print(f" VALORES HARDCODED ({len(result['hardcoded_matches'])} ocorrências):")
|
|
226
|
+
for match in result["hardcoded_matches"]:
|
|
227
|
+
print(f" {match['file']}:{match['line']}")
|
|
228
|
+
print(f" Valor: {match['hardcoded_value']}")
|
|
229
|
+
print(f" Use: var({match['token_name']})")
|
|
230
|
+
print()
|
|
231
|
+
|
|
232
|
+
if not result["tokens_unused"] and not result["hardcoded_matches"]:
|
|
233
|
+
print(" Todos os tokens estão sendo utilizados corretamente!\n")
|
|
234
|
+
|
|
235
|
+
print(f"{'='*50}\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main():
|
|
239
|
+
parser = argparse.ArgumentParser(
|
|
240
|
+
description="Validador de uso de design tokens.",
|
|
241
|
+
epilog="""Exemplos:
|
|
242
|
+
python3 validate_tokens.py --design .hefesto/DESIGN.md --src src/
|
|
243
|
+
python3 validate_tokens.py --design .hefesto/DESIGN.md --src src/ -f json
|
|
244
|
+
""",
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument("--design", required=True, help="Caminho para DESIGN.md")
|
|
247
|
+
parser.add_argument("--src", required=True, help="Diretório fonte para escanear")
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"-f", "--format",
|
|
250
|
+
choices=["table", "json"],
|
|
251
|
+
default="table",
|
|
252
|
+
help="Formato de saída (padrão: table)",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
args = parser.parse_args()
|
|
256
|
+
|
|
257
|
+
if not os.path.isfile(args.design):
|
|
258
|
+
print(f"Erro: DESIGN.md não encontrado: {args.design}", file=sys.stderr)
|
|
259
|
+
sys.exit(1)
|
|
260
|
+
|
|
261
|
+
if not os.path.isdir(args.src):
|
|
262
|
+
print(f"Erro: diretório fonte não encontrado: {args.src}", file=sys.stderr)
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
|
|
265
|
+
result = validate_tokens(args.design, args.src)
|
|
266
|
+
format_report(result, args.format)
|
|
267
|
+
|
|
268
|
+
# Exit code não-zero se há tokens não usados ou valores hardcoded
|
|
269
|
+
if result["tokens_unused"] or result["hardcoded_matches"]:
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
main()
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: hefesto-init
|
|
3
|
+
description: "Inicializa o Hefesto no projeto atual. Use /hefesto-init para criar a estrutura .hefesto/ e começar a organizar o desenvolvimento."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
3
6
|
---
|
|
4
7
|
|
|
5
8
|
# Hefesto Init
|
|
@@ -21,7 +24,8 @@ Inicializa a estrutura `.hefesto/` no projeto atual.
|
|
|
21
24
|
├── ROADMAP.md
|
|
22
25
|
├── STATE.md
|
|
23
26
|
├── config.json
|
|
24
|
-
|
|
27
|
+
├── features/
|
|
28
|
+
└── research/
|
|
25
29
|
```
|
|
26
30
|
4. Preencher PROJECT.md com as respostas do usuário.
|
|
27
31
|
5. Gerar STATE.md inicial com posição "Inicializando".
|
|
@@ -33,13 +37,34 @@ Inicializa a estrutura `.hefesto/` no projeto atual.
|
|
|
33
37
|
"project": { "name": "", "language": "pt-BR" },
|
|
34
38
|
"runtime": "claude",
|
|
35
39
|
"feature": { "id_prefix": "FEAT", "counter": 0 },
|
|
40
|
+
"research": { "id_prefix": "RES", "counter": 0 },
|
|
36
41
|
"lifecycle": { "auto_update_state": true }
|
|
37
42
|
}
|
|
38
43
|
```
|
|
39
|
-
8. Informar o usuário que o projeto foi inicializado e sugerir `/hefesto
|
|
44
|
+
8. Informar o usuário que o projeto foi inicializado e sugerir `/hefesto-new-feature` para criar a primeira feature.
|
|
45
|
+
9. Verificar se o MCP Context7 está configurado (checar se `.mcp.json` existe e contém `context7`). Se não estiver, sugerir a instalação:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
O Hefesto usa o Context7 para consultar documentação atualizada de bibliotecas
|
|
49
|
+
durante pesquisas. Para habilitar, adicione ao .mcp.json do projeto:
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"context7": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "@upstash/context7-mcp"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Se o usuário quiser instalar, criar ou atualizar o `.mcp.json` adicionando a entrada do Context7 (preservando outros MCPs existentes). Se não quiser, seguir sem — o Context7 é opcional.
|
|
40
64
|
|
|
41
65
|
## Notas
|
|
42
66
|
|
|
43
67
|
- Todos os textos gerados devem ser em Português BR.
|
|
44
68
|
- O config.json deve ser atualizado com o nome do projeto informado pelo usuário.
|
|
45
69
|
- Se `.hefesto/` já existir e o usuário não quiser reinicializar, abortar sem modificar nada.
|
|
70
|
+
- O Context7 é opcional mas recomendado — melhora a qualidade das pesquisas com docs versionados.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: hefesto-new-feature
|
|
3
|
+
description: "Cria uma nova feature no Hefesto. Use /hefesto-new-feature para definir uma nova feature com visão, fluxo do usuário, requisitos e fases de implementação."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
3
6
|
---
|
|
4
7
|
|
|
5
8
|
# Hefesto New Feature
|
|
@@ -8,7 +11,7 @@ Cria um novo documento de feature em `.hefesto/features/`.
|
|
|
8
11
|
|
|
9
12
|
## Pré-requisitos
|
|
10
13
|
|
|
11
|
-
Verificar se `.hefesto/` existe. Se não existir, sugerir `/hefesto
|
|
14
|
+
Verificar se `.hefesto/` existe. Se não existir, sugerir `/hefesto-init` primeiro.
|
|
12
15
|
|
|
13
16
|
## O que fazer
|
|
14
17
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: hefesto-update
|
|
3
|
+
description: "Atualiza o Hefesto para a versão mais recente. Use /hefesto-update para atualizar skills, hooks e templates sem perder o estado do projeto."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
disable-model-invocation: true
|
|
3
6
|
---
|
|
4
7
|
|
|
5
8
|
# Hefesto Update
|
|
@@ -8,7 +11,7 @@ Atualiza o toolkit Hefesto para a versão mais recente do npm.
|
|
|
8
11
|
|
|
9
12
|
## Pré-requisitos
|
|
10
13
|
|
|
11
|
-
Verificar se `.hefesto/` existe. Se não existir, informar que o projeto não foi inicializado e sugerir `/hefesto
|
|
14
|
+
Verificar se `.hefesto/` existe. Se não existir, informar que o projeto não foi inicializado e sugerir `/hefesto-init`.
|
|
12
15
|
|
|
13
16
|
## O que fazer
|
|
14
17
|
|
|
@@ -26,6 +29,6 @@ Verificar se `.hefesto/` existe. Se não existir, informar que o projeto não fo
|
|
|
26
29
|
## Notas
|
|
27
30
|
|
|
28
31
|
- O installer preserva `.hefesto/` existente (PROJECT.md, STATE.md, ROADMAP.md, features/, config.json).
|
|
29
|
-
- O que é atualizado:
|
|
32
|
+
- O que é atualizado: skills, hooks, templates.
|
|
30
33
|
- Não usar `--global` a menos que o usuário peça explicitamente.
|
|
31
34
|
- Se o usuário pedir para atualizar um runtime específico (ex: `--gemini`), passar a flag correspondente.
|