wizz-method 2.17.2 → 2.19.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/THIRD-PARTY-NOTICES.md +12 -0
- package/package.json +5 -3
- package/removals.txt +10 -0
- package/skills-registry.yaml +72 -42
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +9 -9
- package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/checklist.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +5 -5
- package/src/bmm-skills/module-help.csv +1 -1
- package/src/bmm-skills/module.yaml +1 -1
- package/src/core-skills/_shared/handoff-protocol.md +5 -3
- package/src/modules/lowticket/skills/wizz-lowticket-funil/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-guia/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-minerador/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-pagina/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-trafego/SKILL.md +1 -1
- package/src/modules/wizz/_shared/cerebro.md +7 -7
- package/src/modules/wizz/_shared/communication-rules.md +1 -1
- package/src/modules/wizz/_shared/despacho-barato.md +1 -1
- package/src/modules/wizz/_shared/encerramento.md +4 -3
- package/src/modules/wizz/_shared/model-ladder.md +10 -0
- package/src/modules/wizz/_shared/organizacao.md +8 -4
- package/src/modules/wizz/_shared/token-economy.md +1 -1
- package/src/modules/wizz/_shared/video-pipeline.md +13 -3
- package/src/modules/wizz/agents/wizz-ads/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-ads/customize.toml +12 -3
- package/src/modules/wizz/agents/wizz-ads/knowledge/playbook.md +36 -0
- package/src/modules/wizz/agents/wizz-copy/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-copy/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +34 -0
- package/src/modules/wizz/agents/wizz-designer/SKILL.md +2 -2
- package/src/modules/wizz/agents/wizz-designer/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-growth/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-growth/customize.toml +33 -4
- package/src/modules/wizz/agents/wizz-growth/knowledge/playbook.md +39 -0
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +7 -9
- package/src/modules/wizz/agents/wizz-maestro/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-memoria/SKILL.md +7 -7
- package/src/modules/wizz/agents/wizz-memoria/customize.toml +5 -6
- package/src/modules/wizz/agents/wizz-qa/SKILL.md +5 -6
- package/src/modules/wizz/agents/wizz-qa/customize.toml +3 -4
- package/src/modules/wizz/agents/wizz-seo/SKILL.md +4 -1
- package/src/modules/wizz/agents/wizz-seo/customize.toml +2 -3
- package/src/modules/wizz/agents/wizz-seo/knowledge/playbook.md +31 -0
- package/src/modules/wizz/agents/wizz-social/SKILL.md +5 -3
- package/src/modules/wizz/agents/wizz-social/customize.toml +12 -3
- package/src/modules/wizz/overrides/wizz-agent-analyst.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-architect.toml +3 -3
- package/src/modules/wizz/overrides/wizz-agent-dev.toml +3 -3
- package/src/modules/wizz/overrides/wizz-agent-pm.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-tech-writer.toml +2 -2
- package/src/modules/wizz/overrides/wizz-agent-ux-designer.toml +2 -2
- package/src/modules/wizz/subagents/codex/wizz-exec-standard.toml +1 -1
- package/src/modules/wizz/subagents/codex/wizz-exec-strong.toml +1 -1
- package/src/modules/wizz/subagents/gemini/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/gemini/wizz-exec-strong.md +1 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-strong.md +1 -1
- package/src/modules/wizz/subagents/spec.yaml +6 -6
- package/src/modules/wizz/subagents/wizz-exec-standard.md +1 -1
- package/src/modules/wizz/subagents/wizz-exec-strong.md +1 -1
- package/src/skills-lib/_shared/complexity-signal.md +8 -9
- package/src/skills-lib/_shared/dispatch-rule.yaml +21 -0
- package/src/skills-lib/{cerebro → wizz-cerebro}/SKILL.md +1 -1
- package/src/skills-lib/{ctc-align → wizz-ctc-align}/SKILL.md +1 -1
- package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/SKILL.md +1 -1
- package/src/skills-lib/{find-skills → wizz-find-skills}/SKILL.md +1 -1
- package/src/skills-lib/wizz-hook-extractor/LICENSE +21 -0
- package/src/skills-lib/wizz-hook-extractor/SKILL.md +56 -0
- package/src/skills-lib/wizz-hook-extractor/references/examples.md +59 -0
- package/src/skills-lib/wizz-hook-extractor/references/formulas.md +136 -0
- package/src/skills-lib/wizz-hook-extractor/scripts/read-post.sh +83 -0
- package/src/skills-lib/wizz-huashu-design/references/sfx-library.md +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/add-music.sh +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/html2pptx.js +45 -37
- package/src/skills-lib/wizz-huashu-design/scripts/mix-voiceover.sh +1 -1
- package/src/skills-lib/wizz-huashu-design/scripts/narrate-pipeline.mjs +11 -5
- package/src/skills-lib/wizz-huashu-design/scripts/test_scripts.py +209 -0
- package/src/skills-lib/wizz-huashu-design/scripts/tts-doubao.mjs +42 -28
- package/src/skills-lib/wizz-ig-profile-optimizer/LICENSE +21 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/SKILL.md +74 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/scripts/capture-profile.sh +41 -0
- package/src/skills-lib/wizz-ig-profile-optimizer/scripts/limits.mjs +82 -0
- package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/SKILL.md +7 -7
- package/src/skills-lib/wizz-launch-readiness/references/02-seguranca.md +3 -3
- package/src/skills-lib/wizz-launch-readiness/references/03-seo-descoberta.md +5 -5
- package/src/skills-lib/wizz-launch-readiness/references/04-analytics-medicao.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/references/05-conteudo-prova-social.md +6 -6
- package/src/skills-lib/wizz-launch-readiness/references/06-legal-lgpd.md +1 -1
- package/src/skills-lib/wizz-launch-readiness/references/08-fusao-priorizacao.md +1 -1
- package/src/skills-lib/wizz-offer-forge/SKILL.md +3 -3
- package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/README.md +1 -1
- package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/SKILL.md +1 -1
- package/src/skills-lib/wizz-router/SKILL.md +3 -3
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +11 -9
- package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/SKILL.md +1 -1
- package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/SKILL.md +1 -1
- package/src/skills-lib/wizz-video-effects/SKILL.md +4 -3
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +3 -3
- package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +15 -1
- package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +20 -0
- package/src/skills-lib/wizz-video-styles/SKILL.md +11 -5
- package/src/skills-lib/wizz-video-styles/data/recipes.json +65 -2
- package/src/skills-lib/wizz-video-styles/data/references.json +386 -1
- package/src/skills-lib/wizz-video-styles/data/styles.json +109 -0
- package/src/skills-lib/wizz-video-styles/references/generated-prompts.md +70 -0
- package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +2 -0
- package/src/skills-lib/wizz-video-styles/references/style-picker.md +8 -4
- package/src/skills-lib/wizz-video-styles/references/styles/tipografia-cinetica.md +68 -0
- package/src/skills-lib/wizz-video-styles/references/styles/ui-morph.md +68 -0
- package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +23 -10
- package/src/skills-lib/wizz-video-styles/scripts/beat_grid.py +68 -0
- package/src/skills-lib/wizz-video-styles/scripts/references.mjs +265 -0
- package/src/skills-lib/wizz-web-security/SKILL.md +1 -1
- package/tools/fetch-assets.mjs +1 -1
- package/tools/hooks/README.md +18 -14
- package/tools/hooks/rtk-rewrite.js +95 -0
- package/tools/hooks/security-defensive-context.js +17 -1
- package/tools/hooks/session-rules.js +25 -5
- package/tools/hooks/wizz-arrumar.js +126 -12
- package/tools/hooks/wizz-artifact-guard.js +41 -7
- package/tools/hooks/wizz-cerebro-lib.js +239 -0
- package/tools/hooks/wizz-close-reminder.js +124 -0
- package/tools/hooks/wizz-debug-lib.js +55 -0
- package/tools/hooks/wizz-decision-trace.js +19 -2
- package/tools/hooks/wizz-feature.js +20 -4
- package/tools/hooks/wizz-router-enforce.js +30 -13
- package/tools/installer/command-text.js +243 -0
- package/tools/installer/commands/catalog.js +2 -0
- package/tools/installer/commands/doctor.js +10 -12
- package/tools/installer/commands/fetch-assets.js +5 -5
- package/tools/installer/commands/install.js +6 -7
- package/tools/installer/commands/resolve.js +17 -17
- package/tools/installer/commands/trace-report.js +40 -37
- package/tools/installer/core/doctor.js +60 -55
- package/tools/installer/core/hooks-settings.js +88 -14
- package/tools/installer/core/hooks-smoke.js +134 -0
- package/tools/installer/core/installer.js +61 -52
- package/tools/installer/core/quick-update.js +2 -3
- package/tools/installer/install-text.js +201 -0
- package/tools/installer/modules/channel-plan.js +2 -2
- package/tools/installer/modules/env-vars.js +5 -6
- package/tools/installer/modules/skills-lib.js +1 -1
- package/tools/installer/ui.js +17 -9
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-pesquisa-e-memoria.md +0 -0
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-rotina-e-captura.md +0 -0
- /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-sessao.md +0 -0
- /package/src/skills-lib/{ctc-align → wizz-ctc-align}/scripts/align.py +0 -0
- /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/README.md +0 -0
- /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/references/KEYWORDS.md +0 -0
- /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/assets/template.html +0 -0
- /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/prompts.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-1-validacao-entrada.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-2-autorizacao.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-3-abuso-volume.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-4-vazamento-resposta.md +0 -0
- /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-5-fusao-plano.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/01-cta-principal.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/02-barra-fixa-mobile.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/03-tempo-de-resposta.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/04-prova-social.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/05-faq-decisao.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/06-imagens-reais.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/07-endereco-como-chegar.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/08-titles.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/09-open-graph.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/10-breadcrumbs.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/11-alt-text.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/12-schema-local.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/13-robots-sitemap.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/14-privacidade-termos.md +0 -0
- /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/15-medicao.md +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Testes (unittest, stdlib) das partes puras de fetch_images.py e verify.py.
|
|
2
|
+
|
|
3
|
+
Rede e browser ficam de fora: a API da Wikimedia e o download são trocados por
|
|
4
|
+
mocks. Rodam via `python3 -m unittest discover` (test/test-python-skills.js).
|
|
5
|
+
"""
|
|
6
|
+
import contextlib
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from unittest import mock
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
15
|
+
|
|
16
|
+
import fetch_images # noqa: E402
|
|
17
|
+
import verify # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeResponse:
|
|
21
|
+
"""Resposta mínima de urlopen usada como context manager."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, payload=b"IMG"):
|
|
24
|
+
self._payload = payload
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def __exit__(self, *exc):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def read(self):
|
|
33
|
+
return self._payload
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def page(title, thumb=None, url=None, lic="CC BY-SA 4.0", artist="<a href='x'>Ana</a> Silva"):
|
|
37
|
+
info = {
|
|
38
|
+
"descriptionurl": "https://commons.example/" + title,
|
|
39
|
+
"extmetadata": {"LicenseShortName": {"value": lic}, "Artist": {"value": artist}},
|
|
40
|
+
}
|
|
41
|
+
if thumb:
|
|
42
|
+
info["thumburl"] = thumb
|
|
43
|
+
if url:
|
|
44
|
+
info["url"] = url
|
|
45
|
+
return {"title": title, "imageinfo": [info]}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SafeNameTest(unittest.TestCase):
|
|
49
|
+
def test_replaces_unsafe_chars(self):
|
|
50
|
+
self.assertEqual(fetch_images._safe("Petronas Towers/KL"), "Petronas_Towers_KL")
|
|
51
|
+
|
|
52
|
+
def test_keeps_word_chars_dash_dot(self):
|
|
53
|
+
self.assertEqual(fetch_images._safe("a-b.c_d"), "a-b.c_d")
|
|
54
|
+
|
|
55
|
+
def test_truncates_to_60(self):
|
|
56
|
+
self.assertEqual(len(fetch_images._safe("x" * 200)), 60)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FetchTest(unittest.TestCase):
|
|
60
|
+
def setUp(self):
|
|
61
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
62
|
+
self.addCleanup(self.tmp.cleanup)
|
|
63
|
+
self.out = self.tmp.name
|
|
64
|
+
|
|
65
|
+
def run_fetch(self, pages, query="Langkawi beach", count=2, width=1600, urlopen=None):
|
|
66
|
+
data = {"query": {"pages": pages}}
|
|
67
|
+
stack = contextlib.ExitStack()
|
|
68
|
+
stack.enter_context(mock.patch.object(fetch_images, "_api_get", return_value=data))
|
|
69
|
+
opener = urlopen or (lambda req, timeout=0: FakeResponse())
|
|
70
|
+
stack.enter_context(mock.patch.object(fetch_images.urllib.request, "urlopen", side_effect=opener))
|
|
71
|
+
with stack:
|
|
72
|
+
buf = io.StringIO()
|
|
73
|
+
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(io.StringIO()):
|
|
74
|
+
got = fetch_images.fetch(query, self.out, count, width)
|
|
75
|
+
return got, buf.getvalue()
|
|
76
|
+
|
|
77
|
+
def test_downloads_thumb_and_prints_license_and_clean_artist(self):
|
|
78
|
+
pages = {"1": page("File:Beach one.jpg", thumb="https://up.example/t/beach.jpg?x=1")}
|
|
79
|
+
got, out = self.run_fetch(pages)
|
|
80
|
+
self.assertEqual(len(got), 1)
|
|
81
|
+
self.assertTrue(os.path.exists(got[0]))
|
|
82
|
+
self.assertTrue(got[0].startswith(self.out))
|
|
83
|
+
self.assertTrue(got[0].endswith(".jpg"), "extensão vem da URL sem a query string")
|
|
84
|
+
self.assertIn("CC BY-SA 4.0", out)
|
|
85
|
+
self.assertIn("Ana Silva", out)
|
|
86
|
+
self.assertNotIn("<a", out, "tags HTML do autor são removidas")
|
|
87
|
+
self.assertIn("https://commons.example/File:Beach one.jpg", out)
|
|
88
|
+
|
|
89
|
+
def test_filename_has_query_and_title_without_file_prefix(self):
|
|
90
|
+
got, _ = self.run_fetch({"1": page("File:Nice view.png", thumb="https://up.example/x.png")}, query="Petronas Towers")
|
|
91
|
+
name = os.path.basename(got[0])
|
|
92
|
+
self.assertTrue(name.startswith("Petronas_Towers_"), name)
|
|
93
|
+
self.assertNotIn("File_", name)
|
|
94
|
+
self.assertTrue(name.endswith(".png"))
|
|
95
|
+
|
|
96
|
+
def test_falls_back_to_url_when_no_thumb_and_default_ext_jpg(self):
|
|
97
|
+
got, _ = self.run_fetch({"1": page("File:a", url="https://up.example/noext")})
|
|
98
|
+
self.assertEqual(len(got), 1)
|
|
99
|
+
self.assertTrue(got[0].endswith(".jpg"))
|
|
100
|
+
|
|
101
|
+
def test_skips_pages_without_any_image_url(self):
|
|
102
|
+
got, _ = self.run_fetch({"1": page("File:nada")})
|
|
103
|
+
self.assertEqual(got, [])
|
|
104
|
+
|
|
105
|
+
def test_respects_count(self):
|
|
106
|
+
pages = {str(i): page(f"File:img{i}.jpg", thumb=f"https://up.example/{i}.jpg") for i in range(5)}
|
|
107
|
+
got, _ = self.run_fetch(pages, count=2)
|
|
108
|
+
self.assertEqual(len(got), 2)
|
|
109
|
+
|
|
110
|
+
def test_missing_metadata_uses_question_mark(self):
|
|
111
|
+
bare = {"title": "File:x.jpg", "imageinfo": [{"thumburl": "https://up.example/x.jpg"}]}
|
|
112
|
+
_, out = self.run_fetch({"1": bare})
|
|
113
|
+
self.assertIn("| ? | ?", out)
|
|
114
|
+
|
|
115
|
+
def test_api_failure_returns_empty_list(self):
|
|
116
|
+
with mock.patch.object(fetch_images, "_api_get", side_effect=OSError("boom")):
|
|
117
|
+
with contextlib.redirect_stderr(io.StringIO()) as err:
|
|
118
|
+
got = fetch_images.fetch("q", self.out, 2, 800)
|
|
119
|
+
self.assertEqual(got, [])
|
|
120
|
+
self.assertIn("FAIL search", err.getvalue())
|
|
121
|
+
|
|
122
|
+
def test_no_pages_returns_empty_and_warns(self):
|
|
123
|
+
with mock.patch.object(fetch_images, "_api_get", return_value={"query": {}}):
|
|
124
|
+
with contextlib.redirect_stderr(io.StringIO()) as err:
|
|
125
|
+
got = fetch_images.fetch("q", self.out, 2, 800)
|
|
126
|
+
self.assertEqual(got, [])
|
|
127
|
+
self.assertIn("EMPTY", err.getvalue())
|
|
128
|
+
|
|
129
|
+
def test_download_failure_is_skipped_not_fatal(self):
|
|
130
|
+
def opener(req, timeout=0):
|
|
131
|
+
raise OSError("download caiu")
|
|
132
|
+
|
|
133
|
+
got, _ = self.run_fetch({"1": page("File:a.jpg", thumb="https://up.example/a.jpg")}, urlopen=opener)
|
|
134
|
+
self.assertEqual(got, [])
|
|
135
|
+
|
|
136
|
+
def test_requests_send_compliant_user_agent_and_width(self):
|
|
137
|
+
seen = {}
|
|
138
|
+
|
|
139
|
+
def fake_api(params):
|
|
140
|
+
seen.update(params)
|
|
141
|
+
return {"query": {"pages": {}}}
|
|
142
|
+
|
|
143
|
+
with mock.patch.object(fetch_images, "_api_get", side_effect=fake_api):
|
|
144
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
145
|
+
fetch_images.fetch("Georgetown", self.out, 3, 1200)
|
|
146
|
+
self.assertEqual(seen["gsrsearch"], "Georgetown")
|
|
147
|
+
self.assertEqual(seen["gsrlimit"], 3)
|
|
148
|
+
self.assertEqual(seen["iiurlwidth"], 1200)
|
|
149
|
+
self.assertEqual(seen["gsrnamespace"], 6)
|
|
150
|
+
self.assertIn("huashu-design", fetch_images.UA)
|
|
151
|
+
|
|
152
|
+
def test_module_clears_proxy_env(self):
|
|
153
|
+
for key in ("ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "all_proxy"):
|
|
154
|
+
self.assertNotIn(key, os.environ)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class MainTest(unittest.TestCase):
|
|
158
|
+
def run_main(self, argv, fetch_result):
|
|
159
|
+
with mock.patch.object(sys, "argv", ["fetch_images.py", *argv]):
|
|
160
|
+
with mock.patch.object(fetch_images, "fetch", return_value=fetch_result) as fetch:
|
|
161
|
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
162
|
+
try:
|
|
163
|
+
fetch_images.main()
|
|
164
|
+
code = None
|
|
165
|
+
except SystemExit as exc:
|
|
166
|
+
code = exc.code
|
|
167
|
+
return code, fetch
|
|
168
|
+
|
|
169
|
+
def test_exit_1_when_nothing_downloaded(self):
|
|
170
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
171
|
+
code, _ = self.run_main(["--query", "a", "b", "--out", os.path.join(tmp, "img")], [])
|
|
172
|
+
self.assertEqual(code, 1)
|
|
173
|
+
|
|
174
|
+
def test_success_creates_out_dir_and_passes_defaults(self):
|
|
175
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
176
|
+
out = os.path.join(tmp, "novo", "img")
|
|
177
|
+
code, fetch = self.run_main(["--query", "a", "b", "--out", out], ["f.jpg"])
|
|
178
|
+
self.assertIsNone(code)
|
|
179
|
+
self.assertTrue(os.path.isdir(out))
|
|
180
|
+
self.assertEqual(fetch.call_count, 2, "um fetch por query")
|
|
181
|
+
fetch.assert_any_call("a", out, 2, 1600)
|
|
182
|
+
|
|
183
|
+
def test_custom_count_and_width(self):
|
|
184
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
185
|
+
out = os.path.join(tmp, "img")
|
|
186
|
+
_, fetch = self.run_main(["--query", "x", "--out", out, "--count", "4", "--width", "800"], ["f"])
|
|
187
|
+
fetch.assert_called_once_with("x", out, 4, 800)
|
|
188
|
+
|
|
189
|
+
def test_requires_query_and_out(self):
|
|
190
|
+
with mock.patch.object(sys, "argv", ["fetch_images.py"]):
|
|
191
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
192
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
193
|
+
fetch_images.main()
|
|
194
|
+
self.assertEqual(ctx.exception.code, 2)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ParseViewportTest(unittest.TestCase):
|
|
198
|
+
def test_parses_width_x_height(self):
|
|
199
|
+
self.assertEqual(verify.parse_viewport("1440x900"), {"width": 1440, "height": 900})
|
|
200
|
+
self.assertEqual(verify.parse_viewport("375x667"), {"width": 375, "height": 667})
|
|
201
|
+
|
|
202
|
+
def test_rejects_malformed(self):
|
|
203
|
+
for bad in ("1440", "axb", "1x2x3", ""):
|
|
204
|
+
with self.assertRaises(ValueError, msg=bad):
|
|
205
|
+
verify.parse_viewport(bad)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
unittest.main()
|
|
@@ -28,8 +28,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
28
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
29
|
const SKILL_ROOT = path.resolve(__dirname, '..');
|
|
30
30
|
|
|
31
|
-
function loadEnv() {
|
|
32
|
-
const envPath = path.join(SKILL_ROOT, '.env');
|
|
31
|
+
function loadEnv(envPath = path.join(SKILL_ROOT, '.env')) {
|
|
33
32
|
if (!fs.existsSync(envPath)) return;
|
|
34
33
|
const text = fs.readFileSync(envPath, 'utf8');
|
|
35
34
|
for (const line of text.split('\n')) {
|
|
@@ -45,7 +44,6 @@ function loadEnv() {
|
|
|
45
44
|
if (!(key in process.env)) process.env[key] = val;
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
|
-
loadEnv();
|
|
49
47
|
|
|
50
48
|
function parseArgs(argv) {
|
|
51
49
|
const args = { speed: '1.0', encoding: 'mp3' };
|
|
@@ -90,16 +88,9 @@ function getDuration(filePath) {
|
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const endpoint = process.env.DOUBAO_TTS_ENDPOINT || 'https://openspeech.bytedance.com/api/v1/tts';
|
|
97
|
-
const voiceId = voice || process.env.DOUBAO_TTS_VOICE_ID;
|
|
98
|
-
|
|
99
|
-
if (!apiKey) throw new Error('缺 DOUBAO_TTS_API_KEY(检查 .env)');
|
|
100
|
-
if (!voiceId) throw new Error('缺 DOUBAO_TTS_VOICE_ID(检查 .env 或用 --voice 传)');
|
|
101
|
-
|
|
102
|
-
const body = {
|
|
91
|
+
/** Corpo da requisição à API do Doubao (puro, sem rede). */
|
|
92
|
+
function buildTtsBody({ cluster, voiceId, encoding, speed, text, reqid = randomUUID() }) {
|
|
93
|
+
return {
|
|
103
94
|
app: { cluster },
|
|
104
95
|
user: { uid: 'wizz-huashu-design' },
|
|
105
96
|
audio: {
|
|
@@ -108,11 +99,36 @@ async function tts({ text, voice, speed, encoding }) {
|
|
|
108
99
|
speed_ratio: parseFloat(speed),
|
|
109
100
|
},
|
|
110
101
|
request: {
|
|
111
|
-
reqid
|
|
102
|
+
reqid,
|
|
112
103
|
text,
|
|
113
104
|
operation: 'query',
|
|
114
105
|
},
|
|
115
106
|
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Valida a resposta JSON do Doubao e devolve o áudio decodificado (Buffer). */
|
|
110
|
+
function parseTtsResponse(json) {
|
|
111
|
+
// 豆包标准返回:{ code, message, data: "<base64 audio>", ... }
|
|
112
|
+
// code === 3000 表示成功
|
|
113
|
+
if (json.code !== undefined && json.code !== 3000) {
|
|
114
|
+
throw new Error(`API 返回错误 code=${json.code} msg=${json.message || JSON.stringify(json)}`);
|
|
115
|
+
}
|
|
116
|
+
if (!json.data) {
|
|
117
|
+
throw new Error(`API 响应无 data 字段:${JSON.stringify(json).slice(0, 500)}`);
|
|
118
|
+
}
|
|
119
|
+
return Buffer.from(json.data, 'base64');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function tts({ text, voice, speed, encoding }) {
|
|
123
|
+
const apiKey = process.env.DOUBAO_TTS_API_KEY;
|
|
124
|
+
const cluster = process.env.DOUBAO_TTS_CLUSTER || 'volcano_icl';
|
|
125
|
+
const endpoint = process.env.DOUBAO_TTS_ENDPOINT || 'https://openspeech.bytedance.com/api/v1/tts';
|
|
126
|
+
const voiceId = voice || process.env.DOUBAO_TTS_VOICE_ID;
|
|
127
|
+
|
|
128
|
+
if (!apiKey) throw new Error('缺 DOUBAO_TTS_API_KEY(检查 .env)');
|
|
129
|
+
if (!voiceId) throw new Error('缺 DOUBAO_TTS_VOICE_ID(检查 .env 或用 --voice 传)');
|
|
130
|
+
|
|
131
|
+
const body = buildTtsBody({ cluster, voiceId, encoding, speed, text });
|
|
116
132
|
|
|
117
133
|
const res = await fetch(endpoint, {
|
|
118
134
|
method: 'POST',
|
|
@@ -128,16 +144,7 @@ async function tts({ text, voice, speed, encoding }) {
|
|
|
128
144
|
throw new Error(`HTTP ${res.status}: ${errText.slice(0, 500)}`);
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
|
|
132
|
-
// 豆包标准返回:{ code, message, data: "<base64 audio>", ... }
|
|
133
|
-
// code === 3000 表示成功
|
|
134
|
-
if (json.code !== undefined && json.code !== 3000) {
|
|
135
|
-
throw new Error(`API 返回错误 code=${json.code} msg=${json.message || JSON.stringify(json)}`);
|
|
136
|
-
}
|
|
137
|
-
if (!json.data) {
|
|
138
|
-
throw new Error(`API 响应无 data 字段:${JSON.stringify(json).slice(0, 500)}`);
|
|
139
|
-
}
|
|
140
|
-
return Buffer.from(json.data, 'base64');
|
|
147
|
+
return parseTtsResponse(await res.json());
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
async function main() {
|
|
@@ -178,7 +185,14 @@ async function main() {
|
|
|
178
185
|
console.log(JSON.stringify(result));
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
// Só executa quando chamado como script; importar o módulo (testes) não lê .env nem chama a API.
|
|
189
|
+
const isMain = process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
190
|
+
if (isMain) {
|
|
191
|
+
loadEnv();
|
|
192
|
+
main().catch((err) => {
|
|
193
|
+
console.error(`TTS 失败:${err.message}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { parseArgs, loadEnv, buildTtsBody, parseTtsResponse };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey Bulaev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wizz-ig-profile-optimizer
|
|
3
|
+
description: Audits and rewrites an Instagram profile so a visitor decides to follow. Captures the public profile by the @ (photo, NAME field, @, bio, link, first grid rows), scores the nine parts (photo, NAME, @, bio, link, category, highlights, first nine of the grid, pinned posts) against the profile's goal (grow, sell, land clients, authority), and delivers a scorecard, fixes ranked by impact, before and after rewrites inside Instagram's limits (bio 150, NAME 30), a highlights plan and the pins chosen by real metrics when Buffer is connected. Use when someone asks to review, fix or optimise an Instagram profile or bio. Triggers - arruma minha bio, revisa meu perfil do instagram, o que colocar nos destaques, quais posts fixar, meu perfil não converte seguidor. Does not write captions or scripts (that is wizz-social-content) and does not design the highlight covers (that is wizz-canvas-design).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wizz-ig-profile-optimizer: the header that earns the follow
|
|
7
|
+
|
|
8
|
+
The follow decision happens on the header and the first rows of the grid: photo, NAME, bio, link, then the grid at a glance. This skill reads that as a stranger would, scores it, and rewrites what fails.
|
|
9
|
+
|
|
10
|
+
All text delivered is Brazilian Portuguese with "você", and no em dash in anything public (bio, NAME, highlight names, pinned captions).
|
|
11
|
+
|
|
12
|
+
## Inputs
|
|
13
|
+
|
|
14
|
+
- The @ (preferred), or a screenshot of the header and the first grid rows.
|
|
15
|
+
- The goal of the profile: **crescer**, **vender**, **conseguir cliente** or **autoridade**. It changes the link, the bio's last line and the pins. Ask once if it is not clear.
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Read the profile.** `scripts/capture-profile.sh <@> <out_dir>` (out_dir under `_wizz-output/`) saves `profile.png` and `profile.json` (NAME, bio and link as text, follower and post counts). **Open the image.** Logged out, Instagram may hide highlights, the category label and the pin marks: ask for a screenshot of those parts instead of guessing them.
|
|
20
|
+
2. **Pull what the Method already knows**, before judging:
|
|
21
|
+
- Positioning, audience and tone from the project's Cérebro (`/wizz-cerebro ver`).
|
|
22
|
+
- Highlights already made in the project (for example a `posts/highlights/` folder).
|
|
23
|
+
- The offer from `wizz-offer-forge`, when one exists: the link and one pin depend on it.
|
|
24
|
+
- Real performance: with Buffer connected, `get_aggregated_post_metrics` and the post list give the best posts by saves, shares and reach. Without it, ask which posts did best; never pick pins by guess.
|
|
25
|
+
3. **Score the nine parts** (table below): passa / ajustar / reprova, one line of reason each.
|
|
26
|
+
4. **Rewrite what fails**, in order of impact: NAME, bio, pins first.
|
|
27
|
+
- **NAME** = name + the word people type in search, up to 30 characters ("Ana Souza | Nutrição"). The NAME field is what Instagram search indexes, not the @.
|
|
28
|
+
- **Bio** up to 150 = who it helps + what it posts about + one proof or specific. Lead with what the follower gets, not the job title. One anchor emoji at most; line breaks are fine.
|
|
29
|
+
- **Link**: one, matched to the goal (offer, booking, newsletter); a link hub only when several are really needed.
|
|
30
|
+
- **Category** (business or creator account): one label that reinforces the bio.
|
|
31
|
+
- **Highlights**: 4 to 6, left to right in the order of the visitor's next question (Comece aqui, Resultados, Oferta, Dúvidas, Sobre), names short enough to show whole, covers in one style.
|
|
32
|
+
- **First nine**: judged as a set (consistent look, readable at a glance, range without chaos); say which tiles to move or replace.
|
|
33
|
+
- **Pins** (up to 3), by goal: a top performer, the offer, a "pra quem é" post.
|
|
34
|
+
5. **Check every rewrite**: `node scripts/limits.mjs --bio "..." --name "..." [--handle "..."] [--highlight "..."]`. It fails on bio over 150, NAME over 30, a NAME with no search word, an @ with invalid characters, a highlight name that gets cut, an em dash, or AI words. Nothing is delivered until it passes. Then run bio and NAME through `wizz-humanizer`.
|
|
35
|
+
6. **Header test.** Reading only photo + NAME + bio + link + first grid row: would a stranger know who this is for and follow? Answer yes or no, and why.
|
|
36
|
+
|
|
37
|
+
## Scorecard
|
|
38
|
+
|
|
39
|
+
| # | Part | Passes when |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 1 | Foto | face (or mark) fills the circle, recent, high contrast, readable small |
|
|
42
|
+
| 2 | NOME | name + search word, up to 30 |
|
|
43
|
+
| 3 | @ | short, matches the brand, no numbers or underscores when avoidable |
|
|
44
|
+
| 4 | Bio | value + topic + proof in up to 150, no cliché opener, no emoji storm |
|
|
45
|
+
| 5 | Link | one, goal-matched; the top link carries the click |
|
|
46
|
+
| 6 | Categoria | set, one niche label that reinforces the bio |
|
|
47
|
+
| 7 | Destaques | 4 to 6, ordered by the visitor's next question, one cover style, names not cut |
|
|
48
|
+
| 8 | 9 primeiras | consistent look, readable, shows range; weak tiles moved or replaced |
|
|
49
|
+
| 9 | Fixados | up to 3, each the best proof for the goal, chosen by metrics |
|
|
50
|
+
|
|
51
|
+
## Delivery
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
👤 PERFIL LIDO: @ · seguidores · posts · objetivo · como foi lido (captura / print)
|
|
55
|
+
📊 PLACAR: 9 linhas, passa | ajustar | reprova + motivo
|
|
56
|
+
🔧 CORREÇÕES POR IMPACTO: lista curta
|
|
57
|
+
✍️ ANTES → DEPOIS: NOME, bio, link, categoria (só o que reprovou), com a contagem do limits.mjs
|
|
58
|
+
⭐ DESTAQUES: ordem, nomes, o que vai em cada um
|
|
59
|
+
📌 FIXADOS: 3 posts + por quê (métrica)
|
|
60
|
+
🧪 TESTE DO CABEÇALHO: sim/não + motivo
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Hand-off
|
|
64
|
+
|
|
65
|
+
- Highlight covers and the pinned posts' art: `wizz-canvas-design`.
|
|
66
|
+
- Captions of the pinned posts: `wizz-social-content`.
|
|
67
|
+
- The offer behind the link: `wizz-offer-forge`.
|
|
68
|
+
|
|
69
|
+
## Hard rules
|
|
70
|
+
|
|
71
|
+
- Never ship a bio or NAME that fails `limits.mjs`, and never a truncated highlight name.
|
|
72
|
+
- Never invent proof for the bio (a number, a client count, a result): ask, or leave a slot.
|
|
73
|
+
- Never log in to Instagram on the user's behalf; a logged-out capture plus their screenshots is enough.
|
|
74
|
+
- Highlights and grid are judged as sets: fix order and consistency, not only single items.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Captures a public Instagram profile as a logged-out visitor sees it, with agent-browser.
|
|
3
|
+
# Usage: capture-profile.sh <@handle or handle> <out_dir>
|
|
4
|
+
# → <out_dir>/profile.png (phone-width, header + first rows of the grid)
|
|
5
|
+
# <out_dir>/profile.json (page title, meta description with counts, header text: NAME, bio, link)
|
|
6
|
+
# Logged out, Instagram may hide highlights, the category label and the pin marks: ask for a
|
|
7
|
+
# screenshot of those. Exit 2 = usage or missing tool; 1 = page did not load.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
[ $# -eq 2 ] || { echo "uso: capture-profile.sh <@perfil> <pasta de saída>" >&2; exit 2; }
|
|
10
|
+
handle="${1#@}"; out="$2"
|
|
11
|
+
[[ "$handle" =~ ^[A-Za-z0-9._]{1,30}$ ]] || { echo "@ inválido: $1" >&2; exit 2; }
|
|
12
|
+
command -v agent-browser >/dev/null || { echo "falta agent-browser no PATH (skill wizz-agent-browser)" >&2; exit 2; }
|
|
13
|
+
mkdir -p "$out"
|
|
14
|
+
trap 'agent-browser close >/dev/null 2>&1 || true' EXIT
|
|
15
|
+
|
|
16
|
+
agent-browser set viewport 430 1400 >/dev/null
|
|
17
|
+
agent-browser open "https://www.instagram.com/$handle/" >/dev/null
|
|
18
|
+
agent-browser wait 3000 >/dev/null
|
|
19
|
+
# Cookie banner (pt or en), then Escape closes the sign-up prompt that covers the grid.
|
|
20
|
+
agent-browser find text "Recusar cookies opcionais" click >/dev/null 2>&1 \
|
|
21
|
+
|| agent-browser find text "Decline optional cookies" click >/dev/null 2>&1 || true
|
|
22
|
+
agent-browser wait 1500 >/dev/null
|
|
23
|
+
agent-browser press Escape >/dev/null 2>&1 || true
|
|
24
|
+
agent-browser wait 800 >/dev/null
|
|
25
|
+
|
|
26
|
+
agent-browser eval 'JSON.stringify({
|
|
27
|
+
title: document.title,
|
|
28
|
+
description: document.querySelector("meta[property=\"og:description\"]")?.content ?? null,
|
|
29
|
+
header: document.querySelector("header")?.innerText ?? null
|
|
30
|
+
})' > "$out/profile.raw"
|
|
31
|
+
node -e '
|
|
32
|
+
const fs = require("node:fs");
|
|
33
|
+
const [raw, out] = process.argv.slice(1);
|
|
34
|
+
let data = JSON.parse(fs.readFileSync(raw, "utf8"));
|
|
35
|
+
if (typeof data === "string") data = JSON.parse(data);
|
|
36
|
+
if (!data.header) { console.error("perfil não carregou (privado, inexistente ou bloqueado)"); process.exit(1); }
|
|
37
|
+
fs.writeFileSync(out, JSON.stringify(data, null, 1));
|
|
38
|
+
' "$out/profile.raw" "$out/profile.json"
|
|
39
|
+
rm -f "$out/profile.raw"
|
|
40
|
+
agent-browser screenshot "$out/profile.png" >/dev/null
|
|
41
|
+
echo "ok: $out/profile.json + profile.png"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Checks Instagram profile text against the platform limits and the Wizz text rules.
|
|
3
|
+
// node limits.mjs --bio "<text>" --name "<text>" [--handle "<text>"] [--highlight "<name>" ...] [--json]
|
|
4
|
+
// Exit 0 = everything fits; 1 = something fails (listed); 2 = usage.
|
|
5
|
+
// Counting is conservative: UTF-16 units, so an emoji counts as 2. Instagram's own counter may be
|
|
6
|
+
// more lenient; a text that passes here fits there.
|
|
7
|
+
|
|
8
|
+
const LIMITS = { bio: 150, name: 30, handle: 30 };
|
|
9
|
+
// Not a platform limit: above this a highlight name gets cut under the circle on most phones.
|
|
10
|
+
const HIGHLIGHT_VISIBLE = 15;
|
|
11
|
+
const AI_WORDS = /\b(alavanc\w*|fundamentalmente|potencializ\w*|eleve|elevar|revolucion\w*|game[- ]changer|leverage|elevate)\b/i;
|
|
12
|
+
const EM_DASH = /—/;
|
|
13
|
+
const HANDLE_CHARS = /^[A-Za-z0-9._]+$/;
|
|
14
|
+
|
|
15
|
+
function parse(argv) {
|
|
16
|
+
const options = { highlights: [], json: false };
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const flag = argv[i];
|
|
19
|
+
if (flag === '--json') options.json = true;
|
|
20
|
+
else if (flag === '--highlight') options.highlights.push(argv[++i] ?? '');
|
|
21
|
+
else if (['--bio', '--name', '--handle'].includes(flag)) options[flag.slice(2)] = argv[++i] ?? '';
|
|
22
|
+
else usage(`unknown argument: ${flag}`);
|
|
23
|
+
}
|
|
24
|
+
if (options.bio === undefined && options.name === undefined && options.handle === undefined && options.highlights.length === 0) {
|
|
25
|
+
usage('nothing to check');
|
|
26
|
+
}
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function usage(message) {
|
|
31
|
+
process.stderr.write(`limits: ${message}\nusage: node limits.mjs --bio "..." --name "..." [--handle "..."] [--highlight "..."] [--json]\n`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const graphemes = (text) => [...new Intl.Segmenter('pt-BR', { granularity: 'grapheme' }).segment(text)].length;
|
|
36
|
+
|
|
37
|
+
function checkField(field, text, limit) {
|
|
38
|
+
const problems = [];
|
|
39
|
+
if (text.length > limit) problems.push(`${text.length} caracteres (limite ${limit}, emoji conta 2)`);
|
|
40
|
+
if (EM_DASH.test(text)) problems.push('travessão (—) em texto público');
|
|
41
|
+
const ai = text.match(AI_WORDS);
|
|
42
|
+
if (ai) problems.push(`palavra de IA: "${ai[0]}"`);
|
|
43
|
+
return { field, text, length: text.length, graphemes: graphemes(text), limit, ok: problems.length === 0, problems };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function check(options) {
|
|
47
|
+
const results = [];
|
|
48
|
+
if (options.bio !== undefined) results.push(checkField('bio', options.bio, LIMITS.bio));
|
|
49
|
+
if (options.name !== undefined) {
|
|
50
|
+
const name = checkField('nome', options.name, LIMITS.name);
|
|
51
|
+
if (!/[|·,\-]/.test(options.name) && options.name.trim().split(/\s+/).length < 3) {
|
|
52
|
+
name.problems.push('NOME sem palavra-chave de busca (ex.: "Ana Souza | Nutrição")');
|
|
53
|
+
name.ok = false;
|
|
54
|
+
}
|
|
55
|
+
results.push(name);
|
|
56
|
+
}
|
|
57
|
+
if (options.handle !== undefined) {
|
|
58
|
+
const handle = checkField('@', options.handle.replace(/^@/, ''), LIMITS.handle);
|
|
59
|
+
if (!HANDLE_CHARS.test(handle.text)) {
|
|
60
|
+
handle.problems.push('@ só aceita letras, números, ponto e sublinhado');
|
|
61
|
+
handle.ok = false;
|
|
62
|
+
}
|
|
63
|
+
results.push(handle);
|
|
64
|
+
}
|
|
65
|
+
for (const highlight of options.highlights) {
|
|
66
|
+
const result = checkField('destaque', highlight, HIGHLIGHT_VISIBLE);
|
|
67
|
+
result.problems = result.problems.map((p) => p.replace(/\(limite \d+, emoji conta 2\)/, `(acima de ${HIGHLIGHT_VISIBLE} corta embaixo do círculo)`));
|
|
68
|
+
results.push(result);
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const options = parse(process.argv.slice(2));
|
|
74
|
+
const results = check(options);
|
|
75
|
+
if (options.json) process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
|
|
76
|
+
else {
|
|
77
|
+
for (const r of results) {
|
|
78
|
+
const mark = r.ok ? 'ok ' : 'ERRO';
|
|
79
|
+
process.stdout.write(`${mark} ${r.field}: ${r.length}/${r.limit}${r.problems.length ? ' · ' + r.problems.join(' · ') : ''}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
process.exit(results.every((r) => r.ok) ? 0 : 1);
|
|
@@ -87,7 +87,7 @@ Before the first file of a new project, and on the first use of the method in an
|
|
|
87
87
|
|
|
88
88
|
1. `node _wizz/hooks/wizz-arrumar.js layout` proposes it from what already exists (types: `codigo`, `scripts`, `dados`, `docs`, `midia`, `arquivo`; a type mapped to `.` lives at the root, e.g. a Next.js app at the root).
|
|
89
89
|
2. Show the proposal to the owner in **one line** and record it with `node _wizz/hooks/wizz-arrumar.js layout --gravar [tipo=pasta ...]`. It creates each new folder with a short README.
|
|
90
|
-
3. An existing project with loose files at the root: `wizz-arrumar.js planejar` writes a `.tsv` manifest; review it and run `wizz-arrumar.js aplicar <manifesto> --testes "<test command>" --refs <auto-memory dir>`. It moves, rewrites references in code, docs and memories, runs the tests before and after, and writes `desfazer.sh`.
|
|
90
|
+
3. An existing project with loose files at the root: `wizz-arrumar.js planejar` writes a `.tsv` manifest; review it and run `wizz-arrumar.js aplicar <manifesto> --testes "<test command>" --refs <auto-memory dir>`. It moves, rewrites references in code, docs and memories, runs the tests before and after, and writes an undo: `wizz-arrumar.js desfazer <run folder>` on any OS, or the `desfazer.sh` (`desfazer.cmd` on Windows) shortcut. `--testes` accepts only known test runners (npm/pnpm/yarn/bun test, npx vitest|jest, pytest, go/cargo/make test); any other command needs `--testes-livre`.
|
|
91
91
|
|
|
92
92
|
From then on the `wizz-artifact-guard.js` hook points out any file born loose at the root and the command that moves it. Full duty in the wizz module's `_shared/organizacao.md`.
|
|
93
93
|
|
|
@@ -8,8 +8,8 @@ description: >
|
|
|
8
8
|
ready to launch?", "pre-launch audit", "broad release checklist", "what's left
|
|
9
9
|
before going to production", considering the whole project (not just the site). Output: a diagnosis
|
|
10
10
|
prioritized by severity (red blocks launch, orange high, yellow medium, green ok), never
|
|
11
|
-
blind fixing. Different from security-audit-pentest (hunts vulnerabilities with proof of exploitation)
|
|
12
|
-
and site-launch-kit (executes the 15 rounds for ONE site): this DIAGNOSES the whole project and points to
|
|
11
|
+
blind fixing. Different from wizz-security-audit-pentest (hunts vulnerabilities with proof of exploitation)
|
|
12
|
+
and wizz-site-launch-kit (executes the 15 rounds for ONE site): this DIAGNOSES the whole project and points to
|
|
13
13
|
the right fix owner per finding. Persists the result in a dated section of project-context.md.
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ description: >
|
|
|
17
17
|
|
|
18
18
|
Seven checking areas, each a closed pass: single scope, standardized output, its own cutoff rule. At the end, a merge step consolidates everything into a prioritized diagnosis and writes the summary to the project's `project-context.md`.
|
|
19
19
|
|
|
20
|
-
This is not a build skill (that's `wizz-premium-landing-ui-researcher` / `wizz-taste-skill` / `wizz-agent-dev`), not the vulnerability-hunting methodology (that's `security-audit-pentest`), and it doesn't execute the 15 corrective rounds for a site (that's `site-launch-kit`): this is a broad, prioritized diagnosis to decide whether it's ready to launch.
|
|
20
|
+
This is not a build skill (that's `wizz-premium-landing-ui-researcher` / `wizz-taste-skill` / `wizz-agent-dev`), not the vulnerability-hunting methodology (that's `wizz-security-audit-pentest`), and it doesn't execute the 15 corrective rounds for a site (that's `wizz-site-launch-kit`): this is a broad, prioritized diagnosis to decide whether it's ready to launch.
|
|
21
21
|
|
|
22
22
|
## Stage gate (read BEFORE running)
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ Read the project's stage: `grep -m1 '^stage:' {project-root}/**/project-context.
|
|
|
25
25
|
|
|
26
26
|
- **`mvp` or `production`:** run the full audit. This is exactly what the skill exists for.
|
|
27
27
|
- **`prototype` or no `project-context.md` file:** stop before running. Reply that the project is still too early for a launch audit (little or nothing built yet to audit) and point to the right path for this stage: `wizz-inicio-de-projeto`, `wizz-forge-idea`, or `wizz-decision-maker` to define the product first. Only proceed if the user explicitly insists, and in that case warn that the findings will come back full of PENDING ITEMS due to lack of built surface.
|
|
28
|
-
- **`maintenance`:** normally not the use case (product already launched, running). Only makes sense if the request is about a NEW batch starting from scratch (a big feature, a new product within the same repo): confirm this before running; otherwise, point to `security-audit-pentest` (periodic pentest) or the `wizz-router`'s "360° Audit" mode (broad audit of a codebase already in production, not about a launch event).
|
|
28
|
+
- **`maintenance`:** normally not the use case (product already launched, running). Only makes sense if the request is about a NEW batch starting from scratch (a big feature, a new product within the same repo): confirm this before running; otherwise, point to `wizz-security-audit-pentest` (periodic pentest) or the `wizz-router`'s "360° Audit" mode (broad audit of a codebase already in production, not about a launch event).
|
|
29
29
|
|
|
30
30
|
## Common contract (applies to all 7 areas)
|
|
31
31
|
|
|
@@ -81,8 +81,8 @@ After the 7:
|
|
|
81
81
|
|
|
82
82
|
## Relationship with other skills
|
|
83
83
|
|
|
84
|
-
- **`security-audit-pentest`**: security deep dive. Area 02 here is a light sweep (exposed surface); findings that need proof of exploitation ("how it's exploited") go to `security-audit-pentest`, they aren't redone here.
|
|
85
|
-
- **`site-launch-kit`**: when the surface of the finding is a SITE (CTA, social proof, Open Graph, local schema, robots/sitemap, site LGPD, measurement), the fix owner is `site-launch-kit`. This skill doesn't repeat the 15 rounds: it only points to which round resolves the finding.
|
|
84
|
+
- **`wizz-security-audit-pentest`**: security deep dive. Area 02 here is a light sweep (exposed surface); findings that need proof of exploitation ("how it's exploited") go to `wizz-security-audit-pentest`, they aren't redone here.
|
|
85
|
+
- **`wizz-site-launch-kit`**: when the surface of the finding is a SITE (CTA, social proof, Open Graph, local schema, robots/sitemap, site LGPD, measurement), the fix owner is `wizz-site-launch-kit`. This skill doesn't repeat the 15 rounds: it only points to which round resolves the finding.
|
|
86
86
|
- **`wizz-seo-audit`**: broad ranking/traffic/Core Web Vitals diagnosis. Area 03 here is only basic indexability (can the site be found), not a full SEO audit.
|
|
87
87
|
- **`wizz-router`'s "360° Audit" mode**: a broad audit of a project already in production, by technical area (code, database, design, growth...), without the "we're about to launch" framing or the stage gate. Use wizz-launch-readiness for the launch event; use 360° mode for a mature project's general health.
|
|
88
88
|
|
|
@@ -90,5 +90,5 @@ After the 7:
|
|
|
90
90
|
|
|
91
91
|
- Doesn't fix anything on its own; it points to the fix owner.
|
|
92
92
|
- Doesn't decide GO/NO-GO on behalf of the project owner; delivers the prioritized reading.
|
|
93
|
-
- Doesn't replace `security-audit-pentest` for deep security or `site-launch-kit` for executing the 15 site rounds.
|
|
93
|
+
- Doesn't replace `wizz-security-audit-pentest` for deep security or `wizz-site-launch-kit` for executing the 15 site rounds.
|
|
94
94
|
- Doesn't run on a project with no built surface (discovery/ideation stage).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Area 02 · Security
|
|
2
2
|
|
|
3
|
-
A sweep of the exposed SURFACE before launch, not a hunt for vulnerabilities with proof of exploitation. A finding that requires a concrete payload to prove (mass assignment, IDOR, injection) is NOT dug into here: log the suspicion as 🟠 and point to `security-audit-pentest` as the owner of the deep dive.
|
|
3
|
+
A sweep of the exposed SURFACE before launch, not a hunt for vulnerabilities with proof of exploitation. A finding that requires a concrete payload to prove (mass assignment, IDOR, injection) is NOT dug into here: log the suspicion as 🟠 and point to `wizz-security-audit-pentest` as the owner of the deep dive.
|
|
4
4
|
|
|
5
5
|
## Objective checks
|
|
6
6
|
|
|
@@ -11,11 +11,11 @@ A sweep of the exposed SURFACE before launch, not a hunt for vulnerabilities wit
|
|
|
11
11
|
5. **CORS too open.** `Access-Control-Allow-Origin: *` on an API that accepts credentials/tokens is 🔴; without credentials is 🟡.
|
|
12
12
|
6. **Rate limiting missing on a sensitive route.** Login, signup, password recovery, or an endpoint that costs money (AI, SMS, email) with no limit at all is 🟠; point to `wizz-web-security` as the fixer.
|
|
13
13
|
7. **Dependency with a known critical CVE on the production path.** Reinforces finding #7 from area 01 if it applies specifically to an auth/crypto lib: escalate to 🔴.
|
|
14
|
-
8. **Unproven indication of mass assignment, IDOR, or injection.** An update field that accepts the client's entire object with no allowlist, or a query built by string concatenation: log as 🟠 "suspicion, needs proof of exploitation" and point to `security-audit-pentest`.
|
|
14
|
+
8. **Unproven indication of mass assignment, IDOR, or injection.** An update field that accepts the client's entire object with no allowlist, or a query built by string concatenation: log as 🟠 "suspicion, needs proof of exploitation" and point to `wizz-security-audit-pentest`.
|
|
15
15
|
|
|
16
16
|
## Cutoff rule
|
|
17
17
|
|
|
18
|
-
This area NEVER delivers "how to exploit it" with a real payload: that is exclusively `security-audit-pentest`'s scope. If during the sweep you'd already have the payload ready, still don't include it here: log the suspicion and the reference, to avoid duplicating the two skills' output format.
|
|
18
|
+
This area NEVER delivers "how to exploit it" with a real payload: that is exclusively `wizz-security-audit-pentest`'s scope. If during the sweep you'd already have the payload ready, still don't include it here: log the suspicion and the reference, to avoid duplicating the two skills' output format.
|
|
19
19
|
|
|
20
20
|
## Output format
|
|
21
21
|
|