wizz-method 2.21.0 → 2.22.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.
Files changed (80) hide show
  1. package/THIRD-PARTY-NOTICES.md +18 -0
  2. package/package.json +4 -2
  3. package/skills-registry.yaml +41 -0
  4. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +1 -1
  5. package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +1 -1
  6. package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
  7. package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +8 -1
  8. package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +2 -2
  9. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-03-implement.md +2 -0
  10. package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +1 -1
  11. package/src/core-skills/wizz-party-mode/customize.toml +3 -3
  12. package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +2 -0
  13. package/src/modules/wizz/_shared/cerebro.md +1 -1
  14. package/src/modules/wizz/_shared/organizacao.md +2 -2
  15. package/src/modules/wizz/agents/wizz-copy/SKILL.md +1 -0
  16. package/src/modules/wizz/agents/wizz-copy/customize.toml +5 -0
  17. package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +2 -1
  18. package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +14 -0
  19. package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +12 -0
  20. package/src/skills-lib/wizz-cerebro/SKILL.md +18 -5
  21. package/src/skills-lib/wizz-design-needs/SKILL.md +7 -0
  22. package/src/skills-lib/wizz-design-needs/references/design-state.md +34 -2
  23. package/src/skills-lib/wizz-design-needs/references/gap-analysis.md +1 -1
  24. package/src/skills-lib/wizz-design-needs/scripts/design_state.py +305 -0
  25. package/src/skills-lib/wizz-design-needs/scripts/tests/test_design_state.py +143 -0
  26. package/src/skills-lib/wizz-impeccable/references/design-rules.md +18 -0
  27. package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +2 -0
  28. package/src/skills-lib/wizz-paid-ads/SKILL.md +2 -1
  29. package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +6 -2
  30. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +7 -34
  31. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +2 -0
  32. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/design-sources.yaml +252 -0
  33. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +1 -1
  34. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mining-rubric.md +123 -0
  35. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +10 -4
  36. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/web-access-and-evidence.md +1 -1
  37. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/design_sources.py +221 -0
  38. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/tests/test_design_sources.py +130 -0
  39. package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
  40. package/src/skills-lib/wizz-taste-skill/references/anti-slop-tells.md +8 -0
  41. package/src/skills-lib/wizz-ui-component-curator/SKILL.md +8 -7
  42. package/src/skills-lib/wizz-video-effects/LICENSE-motionmaxxing.txt +202 -0
  43. package/src/skills-lib/wizz-video-effects/SKILL.md +5 -2
  44. package/src/skills-lib/wizz-video-effects/assets/chrome/collect_layout.js +113 -0
  45. package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +35 -0
  46. package/src/skills-lib/wizz-video-effects/references/data-contract.md +30 -1
  47. package/src/skills-lib/wizz-video-effects/references/gates.md +3 -1
  48. package/src/skills-lib/wizz-video-effects/scripts/check_chrome.py +376 -0
  49. package/src/skills-lib/wizz-video-effects/scripts/check_dead_frames.py +193 -0
  50. package/src/skills-lib/wizz-video-effects/scripts/config.py +13 -0
  51. package/src/skills-lib/wizz-video-effects/scripts/test_chrome.py +181 -0
  52. package/src/skills-lib/wizz-video-effects/scripts/test_dead_frames.py +171 -0
  53. package/src/skills-lib/wizz-video-styles/SKILL.md +6 -2
  54. package/src/skills-lib/wizz-video-styles/references/motion-laws.md +62 -0
  55. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +1 -0
  56. package/src/skills-lib/wizz-video-styles/scripts/blind_review.py +159 -0
  57. package/src/skills-lib/wizz-video-styles/scripts/taste.mjs +110 -0
  58. package/src/skills-lib/wizz-video-styles/scripts/test_blind_review.py +82 -0
  59. package/src/skills-lib/wizz-vsl/LICENSE +49 -0
  60. package/src/skills-lib/wizz-vsl/SKILL.md +122 -0
  61. package/src/skills-lib/wizz-vsl/references/assembly-qa.md +123 -0
  62. package/src/skills-lib/wizz-vsl/references/blocks-and-leads.md +149 -0
  63. package/src/skills-lib/wizz-vsl/references/diagnosis.md +95 -0
  64. package/src/skills-lib/wizz-vsl/references/intake.md +200 -0
  65. package/src/skills-lib/wizz-vsl/references/mechanisms.md +138 -0
  66. package/src/skills-lib/wizz-vsl/references/niches.md +40 -0
  67. package/src/skills-lib/wizz-vsl/references/offer.md +91 -0
  68. package/src/skills-lib/wizz-vsl/references/production.md +75 -0
  69. package/tools/hooks/README.md +10 -9
  70. package/tools/hooks/wizz-arrumar.js +1 -0
  71. package/tools/hooks/wizz-artifact-guard.js +31 -5
  72. package/tools/hooks/wizz-cerebro-lib.js +36 -13
  73. package/tools/hooks/wizz-feature-lib.js +4 -1
  74. package/tools/hooks/wizz-feature.js +66 -76
  75. package/tools/hooks/wizz-sprint-archive-lib.js +311 -0
  76. package/tools/installer/core/hooks-settings.js +1 -0
  77. package/tools/installer/core/installer.js +6 -0
  78. package/tools/installer/modules/core-seed.js +72 -0
  79. package/tools/installer/modules/mcp-config.js +3 -0
  80. package/tools/installer/ui.js +16 -26
@@ -0,0 +1,305 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["ruamel.yaml>=0.18"]
4
+ # ///
5
+ """Check design-state.yaml against the mining rubric, and gate implementation.
6
+
7
+ Prints ONLY JSON to stdout.
8
+
9
+ Subcommands:
10
+ validate <state> Structural and rubric checks on needs and discoveries:
11
+ statuses, required reasons, verdict vs score band, entry
12
+ conditions, the cut and the need's floor, one signature per
13
+ surface, dedup links, adaptation on selected discoveries,
14
+ and the behaviour ceiling of the level. Exit 0 when ok,
15
+ 1 when there are findings, 2 on a file it cannot read.
16
+ gate <state> --section <surface>/<section> [--section ...] | --need <id>
17
+ The implementer's gate. Before building a non-trivial
18
+ section, component or behaviour, every need of that
19
+ section must be in a terminal status, and each `selected`
20
+ need must point at a selected discovery. Open, mining or
21
+ evaluated needs block (mine first); a section the map does
22
+ not know blocks as `unplanned` (back to wizz-design-needs).
23
+ `unmet` passes (build from scratch, the research is the
24
+ justification) and `deferred` passes with "do not build
25
+ this behaviour now". Exit 0 pass, 1 blocked, 2 error.
26
+
27
+ The numbers (cut, bands, entry-condition codes) are read from the
28
+ `# mining-rubric:v1` block of mining-rubric.md and the behaviour ceiling from
29
+ the Rule 3 table of derivacao.md. Nothing is restated here.
30
+
31
+ Run with `uv run --no-cache scripts/design_state.py ...`.
32
+ """
33
+
34
+ import argparse
35
+ import json
36
+ import re
37
+ import sys
38
+ from pathlib import Path
39
+
40
+ HERE = Path(__file__).resolve().parent
41
+ SKILLS = HERE.parents[1]
42
+ DERIVATION = HERE.parent / "references" / "derivacao.md"
43
+ RUBRIC_CANDIDATES = [
44
+ SKILLS / "wizz-premium-landing-ui-researcher" / "references" / "mining-rubric.md",
45
+ ]
46
+
47
+ NEED_STATUSES = {"open", "mining", "evaluated", "selected", "unmet", "dropped", "deferred"}
48
+ NEED_TERMINAL = {"selected", "unmet", "dropped", "deferred"}
49
+ NEED_NEEDS_REASON = {"unmet", "dropped", "deferred"}
50
+ DISC_STATUSES = {"proposed", "selected", "rejected", "implemented", "reverted"}
51
+ DISC_NEEDS_REASON = {"rejected", "reverted"}
52
+ DISC_CHOSEN = {"selected", "implemented"}
53
+ ADAPTATION_FIELDS = ("padrao", "por_que", "invariante", "variavel", "reimplementacao")
54
+
55
+
56
+ def emit(payload, code):
57
+ print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
58
+ sys.exit(code)
59
+
60
+
61
+ def yaml_loader():
62
+ from ruamel.yaml import YAML
63
+
64
+ return YAML(typ="safe")
65
+
66
+
67
+ def load_rubric(candidates=None):
68
+ for path in candidates or RUBRIC_CANDIDATES:
69
+ if path.exists():
70
+ text = path.read_text(encoding="utf-8")
71
+ match = re.search(r"```yaml\n(# mining-rubric:v1\n.*?)```", text, re.S)
72
+ if not match:
73
+ raise ValueError(f"{path}: no '# mining-rubric:v1' block")
74
+ return yaml_loader().load(match.group(1))
75
+ raise ValueError("mining-rubric.md not found next to this skill")
76
+
77
+
78
+ def load_ceiling(path=DERIVATION):
79
+ text = path.read_text(encoding="utf-8")
80
+ start = text.index("## Rule 3")
81
+ section = text[start : text.find("\n## ", start + 1)]
82
+ ceiling = {}
83
+ for line in section.splitlines():
84
+ cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
85
+ if len(cells) >= 3:
86
+ level, needs = re.match(r"\d+", cells[0]), re.match(r"\d+", cells[2])
87
+ if level and needs:
88
+ ceiling[int(level.group())] = int(needs.group())
89
+ if len(ceiling) < 5:
90
+ raise ValueError(f"{path}: Rule 3 table does not have the 5 levels")
91
+ return ceiling
92
+
93
+
94
+ def load_state(path):
95
+ with open(path, encoding="utf-8") as handle:
96
+ data = yaml_loader().load(handle)
97
+ if not isinstance(data, dict):
98
+ raise ValueError(f"{path}: not a mapping")
99
+ return data
100
+
101
+
102
+ def band(rubric, total):
103
+ for verdict in rubric["verdicts"]:
104
+ if verdict["min"] <= total <= verdict["max"]:
105
+ return verdict["id"]
106
+ return None
107
+
108
+
109
+ class Findings:
110
+ def __init__(self):
111
+ self.items = []
112
+
113
+ def add(self, code, where, message):
114
+ self.items.append({"code": code, "where": where, "message": message})
115
+
116
+
117
+ def check_needs(needs, out):
118
+ seen = set()
119
+ for need in needs:
120
+ nid = need.get("id")
121
+ if not nid or nid in seen:
122
+ out.add("need_id", str(nid), "need without id or with a duplicate id")
123
+ seen.add(nid)
124
+ status = need.get("status")
125
+ if status not in NEED_STATUSES:
126
+ out.add("need_status", str(nid), f"unknown status {status!r}")
127
+ if status in NEED_NEEDS_REASON and not need.get("reason"):
128
+ out.add("need_reason", str(nid), f"status {status} needs a reason")
129
+ if not need.get("why"):
130
+ out.add("need_why", str(nid), "a need without `why` does not trace to the brief")
131
+
132
+
133
+ def check_scores(disc, rubric, out, where):
134
+ scores = disc.get("scores") or {}
135
+ total = 0
136
+ for criterion, (low, high) in rubric["criteria"].items():
137
+ value = scores.get(criterion)
138
+ if not isinstance(value, int) or not low <= value <= high:
139
+ out.add("score_range", where, f"{criterion}={value!r} outside {low}-{high}")
140
+ return None
141
+ total += value
142
+ if scores.get("total") not in (None, total):
143
+ out.add("score_total", where, f"total {scores.get('total')} != sum {total}")
144
+ return total
145
+
146
+
147
+ def check_verdict(disc, rubric, out, where):
148
+ verdict = disc.get("verdict")
149
+ if disc.get("disqualifiers"):
150
+ unknown = set(disc["disqualifiers"]) - set(rubric["disqualifiers"])
151
+ if unknown:
152
+ out.add("disqualifier_code", where, f"unknown entry-condition codes {sorted(unknown)}")
153
+ if verdict != "DESCARTAR":
154
+ out.add("verdict_band", where, "an unmet entry condition means DESCARTAR")
155
+ return None
156
+ if disc.get("evidence") != "verified" or disc.get("dup_of"):
157
+ return None
158
+ total = check_scores(disc, rubric, out, where)
159
+ if total is None:
160
+ return None
161
+ expected = band(rubric, total)
162
+ capped = verdict == "FORTE" and expected == "ASSINATURA" and disc.get("reason") == rubric.get("signature_cap_reason")
163
+ if verdict != expected and not capped:
164
+ out.add("verdict_band", where, f"score {total} is {expected}, recorded {verdict}")
165
+ return total
166
+
167
+
168
+ def check_chosen(disc, need, total, rubric, out, where):
169
+ if disc.get("evidence") != "verified":
170
+ out.add("selected_unverified", where, "an unverified discovery cannot be selected")
171
+ if disc.get("verdict") == "DESCARTAR" or disc.get("disqualifiers"):
172
+ out.add("selected_discarded", where, "a DESCARTAR discovery cannot be selected")
173
+ floor = max(rubric["cut"], int((need or {}).get("floor") or 0))
174
+ if total is not None and total < floor:
175
+ out.add("selected_below_floor", where, f"score {total} below the floor {floor}")
176
+ adaptation = disc.get("adaptation") or {}
177
+ missing = [field for field in ADAPTATION_FIELDS if not adaptation.get(field)]
178
+ if missing:
179
+ out.add("adaptation_missing", where, f"selected without adaptation fields {missing}")
180
+
181
+
182
+ def check_discoveries(state, rubric, ceiling, out):
183
+ needs = {need.get("id"): need for need in state.get("needs") or []}
184
+ discoveries = state.get("discoveries") or []
185
+ by_id = {disc.get("id"): disc for disc in discoveries}
186
+ signatures, behaviours = {}, {}
187
+ for disc in discoveries:
188
+ where = str(disc.get("id"))
189
+ need = needs.get(disc.get("need"))
190
+ if need is None:
191
+ out.add("discovery_need", where, f"need {disc.get('need')!r} is not in the map")
192
+ status = disc.get("status")
193
+ if status not in DISC_STATUSES:
194
+ out.add("discovery_status", where, f"unknown status {status!r}")
195
+ if status in DISC_NEEDS_REASON and not disc.get("reason"):
196
+ out.add("discovery_reason", where, f"status {status} needs a reason")
197
+ if not disc.get("observed"):
198
+ out.add("observed_missing", where, "no `observed` list: a score must rest on what was seen")
199
+ if disc.get("dup_of"):
200
+ original = by_id.get(disc["dup_of"])
201
+ if not original or original.get("status") not in DISC_CHOSEN or original.get("pattern_key") != disc.get("pattern_key"):
202
+ out.add("dup_of", where, "dup_of must point at a selected discovery with the same pattern_key")
203
+ total = check_verdict(disc, rubric, out, where)
204
+ if status in DISC_CHOSEN:
205
+ check_chosen(disc, need, total, rubric, out, where)
206
+ surface = disc.get("surface") or (need or {}).get("surface")
207
+ if disc.get("verdict") == "ASSINATURA":
208
+ signatures[surface] = signatures.get(surface, 0) + 1
209
+ if (disc.get("layer") or (need or {}).get("layer")) == "comportamento":
210
+ behaviours[surface] = behaviours.get(surface, 0) + 1
211
+ limit = rubric.get("signature_per_surface", 1)
212
+ for surface, count in signatures.items():
213
+ if count > limit:
214
+ out.add("signature_cap", str(surface), f"{count} ASSINATURA selected, limit {limit}")
215
+ level = state.get("nivel")
216
+ if isinstance(level, int) and level in ceiling:
217
+ for surface, count in behaviours.items():
218
+ if count > ceiling[level]:
219
+ out.add("behaviour_ceiling", str(surface), f"{count} behaviours selected, level {level} allows {ceiling[level]}")
220
+ for need in needs.values():
221
+ if need.get("status") == "selected":
222
+ linked = [by_id.get(ref) for ref in need.get("satisfied_by") or []]
223
+ if not any(disc and disc.get("status") in DISC_CHOSEN for disc in linked):
224
+ out.add("need_unlinked", str(need.get("id")), "selected need without a selected discovery in satisfied_by")
225
+
226
+
227
+ def validate(state, rubric, ceiling):
228
+ out = Findings()
229
+ check_needs(state.get("needs") or [], out)
230
+ check_discoveries(state, rubric, ceiling, out)
231
+ return out.items
232
+
233
+
234
+ def gate(state, sections, need_ids):
235
+ needs = state.get("needs") or []
236
+ discoveries = {disc.get("id"): disc for disc in state.get("discoveries") or []}
237
+ targets = []
238
+ for ref in sections:
239
+ surface, _, section = ref.partition("/")
240
+ matched = [n for n in needs if n.get("surface") == surface and (not section or n.get("section") == section)]
241
+ targets.append((ref, matched))
242
+ for nid in need_ids:
243
+ targets.append((nid, [n for n in needs if n.get("id") == nid]))
244
+ results, blocked = [], False
245
+ for ref, matched in targets:
246
+ if not matched:
247
+ results.append({"target": ref, "result": "unplanned", "action": "not in the map: back to wizz-design-needs"})
248
+ blocked = True
249
+ continue
250
+ for need in matched:
251
+ row = {"target": ref, "need": need.get("id"), "status": need.get("status")}
252
+ status = need.get("status")
253
+ if status not in NEED_TERMINAL:
254
+ row.update(result="blocked", action="mine this need first (wizz-ui-component-curator / researcher)")
255
+ blocked = True
256
+ elif status == "selected":
257
+ chosen = [discoveries.get(ref_id) for ref_id in need.get("satisfied_by") or []]
258
+ chosen = [disc for disc in chosen if disc and disc.get("status") in {"selected", "implemented"}]
259
+ if chosen:
260
+ row.update(result="pass", discoveries=[disc.get("id") for disc in chosen])
261
+ else:
262
+ row.update(result="blocked", action="selected need without a selected discovery")
263
+ blocked = True
264
+ elif status == "deferred":
265
+ row.update(result="pass", action="deferred by the level: do not build this behaviour now")
266
+ elif status == "unmet":
267
+ row.update(result="pass", action="unmet: build from scratch, cite the research as the justification")
268
+ else:
269
+ row.update(result="pass", action=need.get("reason") or "dropped")
270
+ results.append(row)
271
+ return blocked, results
272
+
273
+
274
+ def main(argv=None):
275
+ parser = argparse.ArgumentParser(prog="design_state.py")
276
+ sub = parser.add_subparsers(dest="command", required=True)
277
+ check = sub.add_parser("validate")
278
+ check.add_argument("state")
279
+ gating = sub.add_parser("gate")
280
+ gating.add_argument("state")
281
+ gating.add_argument("--section", action="append", default=[])
282
+ gating.add_argument("--need", action="append", default=[])
283
+ try:
284
+ args = parser.parse_args(argv)
285
+ except SystemExit as error:
286
+ emit({"error": "invalid arguments"}, int(error.code or 2))
287
+ try:
288
+ state = load_state(args.state)
289
+ if args.command == "validate":
290
+ rubric, ceiling = load_rubric(), load_ceiling()
291
+ except (OSError, ValueError, ImportError) as error:
292
+ emit({"error": str(error)}, 2)
293
+ except Exception as error: # ruamel parse errors have their own hierarchy
294
+ emit({"error": f"cannot parse: {error}"}, 2)
295
+ if args.command == "validate":
296
+ findings = validate(state, rubric, ceiling)
297
+ emit({"ok": not findings, "findings": findings}, 1 if findings else 0)
298
+ if not args.section and not args.need:
299
+ emit({"error": "gate needs --section <surface>/<section> or --need <id>"}, 2)
300
+ blocked, results = gate(state, args.section, args.need)
301
+ emit({"gate": "blocked" if blocked else "pass", "results": results}, 1 if blocked else 0)
302
+
303
+
304
+ if __name__ == "__main__":
305
+ main()
@@ -0,0 +1,143 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pytest>=8.0", "ruamel.yaml>=0.18"]
4
+ # ///
5
+ """Tests for design_state.py: rubric validation and the implementation gate."""
6
+
7
+ import copy
8
+ import importlib.util
9
+ import json
10
+
11
+ import pytest
12
+ from pathlib import Path
13
+
14
+ SCRIPT = Path(__file__).resolve().parents[1] / "design_state.py"
15
+ spec = importlib.util.spec_from_file_location("design_state", SCRIPT)
16
+ mod = importlib.util.module_from_spec(spec)
17
+ spec.loader.exec_module(mod)
18
+
19
+ RUBRIC = mod.load_rubric()
20
+ CEILING = mod.load_ceiling()
21
+
22
+ ADAPTATION = {
23
+ "padrao": "o título é revelado por uma máscara que sobe com o scroll",
24
+ "por_que": "segura o olho no título",
25
+ "invariante": "a máscara anda mais rápido que o scroll",
26
+ "variavel": ["easing", "duração"],
27
+ "reimplementacao": "tokens do projeto",
28
+ }
29
+
30
+ BASE = {
31
+ "version": 1,
32
+ "nivel": 1,
33
+ "needs": [
34
+ {"id": "b1", "surface": "home", "section": "hero", "layer": "comportamento", "why": "brief editorial", "floor": 7,
35
+ "status": "selected", "satisfied_by": ["d1"]},
36
+ {"id": "s1", "surface": "home", "section": "projetos", "layer": "secao", "why": "portfólio", "floor": 6,
37
+ "status": "open"},
38
+ {"id": "s2", "surface": "home", "section": "contato", "layer": "secao", "why": "captar contato",
39
+ "status": "unmet", "reason": "2 fontes secas"},
40
+ ],
41
+ "discoveries": [
42
+ {"id": "d1", "need": "b1", "surface": "home", "layer": "comportamento", "evidence": "verified",
43
+ "pattern_key": "scroll-mask-reveal", "observed": ["420ms"], "disqualifiers": [],
44
+ "scores": {"aderencia": 3, "execucao": 2, "custo_adaptacao": 2, "custo_performance": 1, "originalidade": 1, "total": 9},
45
+ "verdict": "ASSINATURA", "adaptation": ADAPTATION, "status": "selected"},
46
+ {"id": "d2", "need": "b1", "surface": "home", "layer": "comportamento", "evidence": "verified",
47
+ "pattern_key": "fade-up", "observed": ["fade"], "disqualifiers": ["reduced_motion"],
48
+ "verdict": "DESCARTAR", "status": "rejected", "reason": "ignora reduced motion"},
49
+ ],
50
+ }
51
+
52
+
53
+ def state(**changes):
54
+ data = copy.deepcopy(BASE)
55
+ for key, value in changes.items():
56
+ data[key] = value
57
+ return data
58
+
59
+
60
+ def codes(data):
61
+ return {item["code"] for item in mod.validate(data, RUBRIC, CEILING)}
62
+
63
+
64
+ def test_rubric_and_ceiling_are_read_from_the_skill_files():
65
+ assert RUBRIC["cut"] == 6 and "licenca" in RUBRIC["disqualifiers"]
66
+ assert CEILING[1] == 1 and CEILING[5] == 8
67
+
68
+
69
+ def test_consistent_state_has_no_findings():
70
+ assert mod.validate(state(), RUBRIC, CEILING) == []
71
+
72
+
73
+ def test_verdict_must_match_the_band():
74
+ data = state()
75
+ data["discoveries"][0]["verdict"] = "FORTE"
76
+ assert "verdict_band" in codes(data)
77
+ data["discoveries"][0]["reason"] = RUBRIC["signature_cap_reason"]
78
+ assert "verdict_band" not in codes(data)
79
+
80
+
81
+ def test_entry_condition_forces_descartar_and_blocks_selection():
82
+ data = state()
83
+ data["discoveries"][1]["verdict"] = "UTILITARIO"
84
+ data["discoveries"][1]["status"] = "selected"
85
+ found = codes(data)
86
+ assert {"verdict_band", "selected_discarded"} <= found
87
+
88
+
89
+ def test_selected_below_the_need_floor_and_without_adaptation():
90
+ data = state()
91
+ disc = data["discoveries"][0]
92
+ disc["scores"] = {"aderencia": 2, "execucao": 1, "custo_adaptacao": 2, "custo_performance": 1, "originalidade": 0}
93
+ disc["verdict"] = "UTILITARIO"
94
+ disc["adaptation"] = {"padrao": "x"}
95
+ assert {"selected_below_floor", "adaptation_missing"} <= codes(data)
96
+
97
+
98
+ def test_two_signatures_on_one_surface_and_behaviour_ceiling():
99
+ data = state()
100
+ second = copy.deepcopy(data["discoveries"][0])
101
+ second.update(id="d3", pattern_key="cursor")
102
+ data["discoveries"].append(second)
103
+ assert {"signature_cap", "behaviour_ceiling"} <= codes(data)
104
+
105
+
106
+ def test_reasons_and_dedup_links():
107
+ data = state()
108
+ data["needs"][2]["reason"] = None
109
+ data["discoveries"][1]["reason"] = None
110
+ data["discoveries"].append({"id": "d4", "need": "b1", "evidence": "verified", "observed": ["x"],
111
+ "pattern_key": "other", "dup_of": "d1", "status": "rejected", "reason": "dup"})
112
+ assert {"need_reason", "discovery_reason", "dup_of"} <= codes(data)
113
+
114
+
115
+ def test_gate_passes_selected_and_blocks_open_and_unplanned():
116
+ blocked, results = mod.gate(state(), ["home/hero"], [])
117
+ assert not blocked and results[0]["discoveries"] == ["d1"]
118
+ blocked, results = mod.gate(state(), ["home/projetos"], [])
119
+ assert blocked and results[0]["result"] == "blocked"
120
+ blocked, results = mod.gate(state(), ["home/rodape"], [])
121
+ assert blocked and results[0]["result"] == "unplanned"
122
+
123
+
124
+ def test_gate_lets_unmet_through_with_the_justification():
125
+ blocked, results = mod.gate(state(), [], ["s2"])
126
+ assert not blocked and "from scratch" in results[0]["action"]
127
+
128
+
129
+ def test_cli_exit_codes(tmp_path, capsys):
130
+ from ruamel.yaml import YAML
131
+
132
+ path = tmp_path / "design-state.yaml"
133
+ with open(path, "w", encoding="utf-8") as handle:
134
+ YAML().dump(state(), handle)
135
+ with pytest.raises(SystemExit) as ok:
136
+ mod.main(["validate", str(path)])
137
+ assert ok.value.code == 0 and json.loads(capsys.readouterr().out)["ok"] is True
138
+ with pytest.raises(SystemExit) as blocked:
139
+ mod.main(["gate", str(path), "--section", "home/projetos"])
140
+ assert blocked.value.code == 1 and json.loads(capsys.readouterr().out)["gate"] == "blocked"
141
+ with pytest.raises(SystemExit) as missing:
142
+ mod.main(["validate", str(tmp_path / "nope.yaml")])
143
+ assert missing.value.code == 2
@@ -48,6 +48,24 @@ One hard typographic ceiling you currently miss:
48
48
  **Gemini-specific defect: hard ban.** Never animate `<img>` elements on hover. This includes any `transform` on `:hover` of an image, AND `.group:hover .group-hover\:scale` / `.group:hover .group-hover\:rotate` / `.group:hover .group-hover\:translate` patterns from Tailwind that animate a child image via a parent hover. This is your single most common motion tell; it adds no information (the image isn't an action target) and reads as "AI animated this because it could". If a card needs hover feedback, animate the card's background, border, or shadow. Never the image, never via the image's parent. <!-- rule:skill-interaction-gemini-no-image-hover -->
49
49
  </gemini>
50
50
 
51
+ #### Landing fine-tuning (Wizz addition, moved here from the landing Audit Protocol)
52
+
53
+ Run with real tokens, real typography and real images; these only show once the page is alive.
54
+
55
+ - **Spacing scale.** Section padding and margins follow one scale (4/8/12/16/24/32/48/64/96/128), not 24 here and 28 there.
56
+ - **Heading hierarchy.** H1, H2 and H3 do not compete: each step has clear size or weight contrast.
57
+ - **Leading.** Body `line-height` ≥ 1.5; display headlines ≤ 1.15.
58
+ - **Small uppercase tracking.** Mono and small-caps labels need more tracking (~0.18em).
59
+ - **Optical alignment.** Icons match the visual weight of adjacent text; mono numbers line up on a shared baseline.
60
+ - **Vertical rhythm.** No run of sections without air, no unexplained huge gap.
61
+ - **Colour hierarchy.** Five or more greys for text need a reason; the accent goes only on what is actionable.
62
+ - **Image cropping.** Thumbnails in one grid share an aspect ratio.
63
+ - **Weight contrast.** Heavy headings need a light or regular body to read against.
64
+ - **Content edge cases.** Check a 1-word and a 4-word project name, a 2-line and a 4-line description.
65
+ - **Tab order.** Focus follows the editorial sequence of the page, not a jump between sections.
66
+
67
+ Output of this check: up to 7 adjustments, each as a specific before/after (e.g. "Hero subhead: leading 1.1 → 1.3").
68
+
51
69
  ### New projects only (when no prior work exists)
52
70
 
53
71
  #### Color & Theme
@@ -29,6 +29,8 @@ Use when you need **real interaction**.
29
29
  - Framer Motion or Motion for scroll integration;
30
30
  - GSAP (ScrollTrigger) when scroll orchestrates the scene — 100% free since v3.13, all plugins included.
31
31
 
32
+ **Reference for fluid effects:** when the brief asks for a fluid cursor trail, liquid distortion or ink-like smoke over a Three.js scene, `artcodev/three-fluid-fx` (MIT, npm `three-fluid-fx`, 0.1.0 on 2026-10-24) is a drop-in 2D Stable-Fluids solver with WebGL/GLSL and WebGPU/TSL pipelines. Pointer only: the method does not install it, and an early version means pinning the exact release and checking the mobile cost against the performance targets before adopting it.
33
+
32
34
  **Examples:** a 3D object that follows the cursor; a mouse-reactive particle sphere; a camera that advances on scroll; real-time wireframe terrain; a configurable 3D product; an interactive WebGL background.
33
35
 
34
36
  **Avoid if:**
@@ -86,7 +86,8 @@ LI_LeadGen_CMOs-SaaS_Whitepaper_Mar24
86
86
 
87
87
  **Scaling phase:**
88
88
  - Consolidate budget into winning combinations
89
- - Increase budgets 20-30% at a time
89
+ - On Meta with a bid cap campaign, follow the order in `wizz-lowticket-metodologia` `knowledge/03-campanha-bidcap.md`: bid cap first, then validated creatives, then budget by the ROAS ruler
90
+ - Without a bid cap (other platforms, or a campaign not built on it yet): increase budgets 20-30% at a time
90
91
  - Wait 3-5 days between increases for algorithm learning
91
92
 
92
93
  ---
@@ -11,7 +11,7 @@ Autonomous strategist for premium landing pages, SaaS UI, and advanced visual ex
11
11
 
12
12
  Check for `{output_folder}/design-state.yaml` (default `_wizz-output/`). Cheap check first: `grep -c 'status: open'`. Not there? Run `find . -name design-state.yaml -not -path '*/node_modules/*'` before concluding it does not exist: a map found elsewhere is still the map, so use it and tell the user it is off-place.
13
13
 
14
- - **It exists, with open needs:** you are not deciding what to research. Work **one need per pass**, in the order given, and take `query`, `budget.fontes` and `floor` as given. Read the `proibidos` block before writing any query: a query that lands on a forbidden capability is not run, not even for comparison. Write each finding back under `discoveries` with the `need` id, and move the need to `selected` or `unmet` with a reason. Stop when the needs are exhausted, not when the ideas are.
14
+ - **It exists, with open needs:** you are not deciding what to research. Work **one need per pass**, in the order given, and take `query`, `budget.fontes` and `floor` as given. Read the `proibidos` block before writing any query: a query that lands on a forbidden capability is not run, not even for comparison. Judge each candidate with [mining-rubric](references/mining-rubric.md) (entry conditions, score, verdict, selection) and write it back under `discoveries` with the `need` id. Each need closes on one of the rubric's stop conditions, never on a feeling of having seen enough. After writing, run `uv run --no-cache ../wizz-design-needs/scripts/design_state.py validate {output_folder}/design-state.yaml` and fix its findings before the handoff. Stop when the needs are exhausted, not when the ideas are.
15
15
  - **It exists, no open needs:** the map is done. Do not open new research on your own: report it and hand back to `wizz-design-needs` if something is genuinely missing.
16
16
  - **It does not exist, and the work is non-trivial:** invoke `skill:wizz-design-needs` first. Mining without a map is how a project ends up depending on the user to name every effect, one request at a time.
17
17
  - **It does not exist, and the work is one isolated component the user named:** proceed, and say the map was not created.
@@ -28,6 +28,8 @@ Absolute rule: inspect real sources, log evidence, and recommend options, adapt
28
28
 
29
29
  ## Gate 2: classify the site's level (always, before recommending components)
30
30
 
31
+ The brief and the visual identity belong to `wizz-decision-maker` (`brief` and `identidade` in `design-state.yaml`): when they exist, read objective, audience and references from them instead of asking again, and never redefine palette or typography here. This skill owns only the level classification and the component and motion research.
32
+
31
33
  If essential data is missing, ask at most 5 strategic questions: (1) business/product/offer, (2) target audience, (3) page's main objective, (4) desired visual level, (5) existing visual references. If the user doesn't answer everything, continue with coherent strategic hypotheses. Master rule: ask, infer, execute.
32
34
 
33
35
  Classify the project into one of 5 levels (rules, allowed/forbidden motion, and budget per level in [site-levels](references/site-levels.md)):
@@ -60,7 +62,8 @@ Classify the project into one of 5 levels (rules, allowed/forbidden motion, and
60
62
  | Handoffs to wizz-motion-3d-director and wizz-implementation-planner, final ladder rule | [handoffs](references/handoffs.md) |
61
63
  | SaaS Dashboard Mode and Portfolio / Authority Site Mode | [dashboard-and-portfolio-modes](references/dashboard-and-portfolio-modes.md) |
62
64
  | The mandatory 12-step process and honesty checkpoint | [mandatory-process](references/mandatory-process.md) |
63
- | The 5-phase protocol (inventory, public search, cache, evidence, adaptation) | [source-first-protocol](references/source-first-protocol.md) |
65
+ | The 6-phase protocol (inventory, public search, cache, evidence, adaptation, stop) | [source-first-protocol](references/source-first-protocol.md) |
66
+ | Entry conditions, 0-10 score, verdicts, selection, stop conditions, dedup and the adaptation protocol | [mining-rubric](references/mining-rubric.md) |
64
67
  | Audit Protocol: Pass 1 Taste, Pass 2 Impeccable, Pass 3 Cross-check, Pass 4 A11y/Perf | [audit-protocol](references/audit-protocol.md) |
65
68
  | Component sources (React Bits, Cult UI, Ali Imam, Watermelon, StyleUI, Skiper UI, Bklit UI, Componentry), animation engines (GSAP, anime.js), reference and visual inspiration sources, Clone Policy, Paid Source Policy | [component-sources](references/component-sources.md) |
66
69
  | Default stack, visual direction, palettes, typography, and animation selection rules | [stack-and-visual-direction](references/stack-and-visual-direction.md) |
@@ -68,6 +71,7 @@ Classify the project into one of 5 levels (rules, allowed/forbidden motion, and
68
71
  | Base prompts (complete landing page and 3D scroll hero) | [prompt-templates](references/prompt-templates.md) |
69
72
  | Final response format, Quality Bar, and self-check before closing the delivery | [output-format-and-quality](references/output-format-and-quality.md) |
70
73
  | Direct links to every authorized source | [source-links](references/source-links.md) |
74
+ | The source catalogue as data (kind, access, license, need tags, cache folder), queried with `scripts/design_sources.py list --need <tag>`; cache freshness and refresh with `freshness` / `refresh` | [design-sources.yaml](references/design-sources.yaml) |
71
75
  | Opening web access before mining, BLOCKED, and the evidence gate before delivering | [web-access-and-evidence](references/web-access-and-evidence.md) |
72
76
 
73
77
  Final rule: never put real keys inside the skill (environment variables only), and never deliver a landing page that looks like a template with no positioning.
@@ -4,51 +4,24 @@ The Source-First Protocol takes care of the **beginning** (curating mature sourc
4
4
 
5
5
  Apply 3 passes in order. Each catches a different class of problem: using the first two is redundant only 60% of the time; in the other 40%, one catches what the other missed.
6
6
 
7
- ### Pass 1: Taste Skill (early, before full implementation)
7
+ ### Pass 1: Taste (early, before full implementation)
8
8
 
9
9
  **When to apply:** right after Phase 5 of the Source-First Protocol, when you have a mockup/wireframe of the sections or a first version of the hero implemented. Ideally there's still time to swap a component without high cost.
10
10
 
11
- **Source:** `https://github.com/Leonxlnx/taste-skill.git`
11
+ **Run:** invoke `skill:wizz-taste-skill` in audit mode. Its checklist is the list for this pass: [design-audit](../../wizz-taste-skill/references/design-audit.md) and [anti-slop-tells](../../wizz-taste-skill/references/anti-slop-tells.md), including the landing-page tells of section 9.H. This protocol keeps only when to run it, the angle and the output, so the list has one owner and cannot drift.
12
12
 
13
13
  **Angle:** an anti-AI-generic detector. Key question: *"Could this layout belong to any generic SaaS, or does it have an authorial signature?"*
14
14
 
15
- **What to look for and flag as a problem:**
16
-
17
- - A hero with "big headline + subtitle + 2 side-by-side CTAs" with no identity of its own
18
- - Features in a 3-col grid with cute decorative icons (the "✨ Fast 🚀 Reliable 🔥 Secure" style)
19
- - Saturated color gradients with no purpose (purple-to-pink, blue-to-cyan, the default Tailwind look)
20
- - AI copy: "revolutionize your workflow", "supercharge your business", "10x your results"
21
- - A perfectly symmetrical layout with no editorial tension
22
- - Excessive rounded corners (`rounded-3xl` on everything)
23
- - Card stacks with the default Tailwind `shadow-xl`
24
- - Decorative emoji anywhere other than status microcopy
25
- - Generic testimonial cards with a round avatar + name + title + quoted sentence
26
- - Pricing cards with a centered, gradient "Most Popular" badge
27
-
28
- **Pass output:** a list of up to 5 spots where the landing page looks templated, with a **replacement** suggestion for each one (a component from an authorized source, an editorial decision, or removal). Present it to the user before proceeding.
15
+ **Pass output:** a list of up to 5 spots where the landing page looks templated, with a **replacement** suggestion for each one (a component from an authorized source, an editorial decision, or removal). Present it to the user before proceeding. A replacement that needs a new component reopens the need in `design-state.yaml` instead of starting a fresh search.
29
16
 
30
17
  ### Pass 2: Impeccable (late, with real tokens and content)
31
18
 
32
19
  **When to apply:** after the implementation is assembled with brand tokens, real typography, real images/mockups. Catches problems that only appear once the component is alive, not in a wireframe.
33
20
 
34
- **Source:** `https://github.com/pbakaus/impeccable.git`
21
+ **Run:** invoke `skill:wizz-impeccable` with `critique` (or `polish` when fixing). Its ruleset is the list for this pass: [design-rules](../../wizz-impeccable/references/design-rules.md), including the "Landing fine-tuning" block. Same split as Pass 1: the list lives in the skill, the timing and output live here.
35
22
 
36
23
  **Angle:** classic design chops. Key question: *"Does this look like it was made by a senior designer with 10+ years of editorial background?"*
37
24
 
38
- **What to look for and flag as a problem:**
39
-
40
- - **Spacing**: inconsistent padding/margin between sections (24px here, 28px there, 32px elsewhere). Define and follow a scale (4/8/12/16/24/32/48/64/96/128)
41
- - **Hierarchy**: headings competing with each other for weight/size. H1, H2, H3 should have clear contrast
42
- - **Leading**: `line-height` too tight in body text (<1.5) or too loose in headlines (>1.15)
43
- - **Tracking**: wrong letter-spacing on small uppercase text (mono labels need more tracking, ~0.18em)
44
- - **Optical alignment**: icons with a different visual weight than adjacent text, mono numbers not monospaced vertically
45
- - **Vertical rhythm**: scrolling with no breathing room (section after section with no air) or with unexplainable huge gaps
46
- - **Color hierarchy**: 5+ shades of gray for text with no justification, accent color used on things that aren't actionable
47
- - **Image cropping**: thumbnails with inconsistent aspect ratio between cards
48
- - **Typographic contrast**: heavy headings + heavy body text too (missing a light/regular weight to create contrast)
49
- - **Edge cases**: what happens when the project name has 4 words vs. 1? And when the description has 2 lines vs. 4?
50
- - **Focus states**: does the tab order deliver an editorial sequence, or does it jump around chaotically between sections?
51
-
52
25
  **Pass output:** a list of up to 7 fine-tuning adjustments with specific **before/after** (e.g., "Hero subhead: change leading 1.1 → 1.3" / "Cards grid: align the mono numbering baseline with the card headline baseline").
53
26
 
54
27
  ### Pass 3: Cross-check with references
@@ -82,7 +55,7 @@ agent-browser eval "<js>" # 4. measure tokens (computed styles, etc.)
82
55
  **Diagnosis:**
83
56
 
84
57
  - If the difference is **taste/direction** (conscious brand choices) → ok, keep it
85
- - If the difference is **execution** (the reference has polish yours is missing) → go back to Source-First and find the missing component
58
+ - If the difference is **execution** (the reference has polish yours is missing) → reopen the need in `design-state.yaml` (status `open`, with the discoveries already rejected carried forward) and mine it again; with no state file, go back to Source-First and find the missing component
86
59
  - If the difference is **motion fluidity** → review timing, easing, scale, and animation composition (the Design Motion Principles were probably not applied)
87
60
  - If the difference is **visual density** (the reference is more spaced out and breathes more) → cut elements, don't add
88
61
 
@@ -106,8 +79,8 @@ Not a matter of taste, it's technical quality. Check:
106
79
 
107
80
  | Pass | When | Adjustment cost | Frequency |
108
81
  |---|---|---|---|
109
- | 1. Taste Skill | Wireframe / hero first | Low (component can still be swapped) | Always |
110
- | 2. Impeccable | Implementation with real tokens | Medium (fine CSS) | Always |
82
+ | 1. Taste (`wizz-taste-skill`) | Wireframe / hero first | Low (component can still be swapped) | Always |
83
+ | 2. Impeccable (`wizz-impeccable`) | Implementation with real tokens | Medium (fine CSS) | Always |
111
84
  | 3. Cross-check | Final visual | Variable | Always |
112
85
  | 4. Accessibility + Perf | Before delivering | Medium-high (may require refactor) | Always |
113
86
 
@@ -1,3 +1,5 @@
1
+ > To pick a source, query [design-sources.yaml](design-sources.yaml) with `scripts/design_sources.py list --need <tag>`; this page is the policy and nuance behind those rows, read when a source needs it.
2
+
1
3
  ## Source Access Strategy
2
4
 
3
5
  Prioritize free, open source, public, registry-based sources or sources provided by the user.