wizz-method 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +62 -62
  2. package/SECURITY.md +15 -16
  3. package/THIRD-PARTY-NOTICES.md +49 -49
  4. package/package.json +1 -1
  5. package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
  6. package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
  7. package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
  8. package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
  9. package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
  10. package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +2 -2
  11. package/src/bmm-skills/4-implementation/wizz-code-review/customize.toml +102 -8
  12. package/src/bmm-skills/4-implementation/wizz-code-review/references/review-economy.md +75 -0
  13. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-01-gather-context.md +29 -15
  14. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-02-review.md +13 -10
  15. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-03-triage.md +11 -5
  16. package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-04-present.md +4 -0
  17. package/src/bmm-skills/4-implementation/wizz-quick-dev/customize.toml +103 -7
  18. package/src/bmm-skills/4-implementation/wizz-quick-dev/references/review-economy.md +75 -0
  19. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-04-review.md +43 -18
  20. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-05-present.md +4 -0
  21. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-oneshot.md +2 -2
  22. package/src/core-skills/_shared/handoff-protocol.md +72 -71
  23. package/src/core-skills/wizz-review-adversarial-general/SKILL.md +1 -1
  24. package/src/modules/lowticket/skills/wizz-lowticket-funil/references/copy-das-paginas.md +39 -39
  25. package/src/modules/lowticket/skills/wizz-lowticket-funil/references/mapa-do-funil.md +23 -23
  26. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/CATALOGO.md +72 -72
  27. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/01-principios.md +10 -10
  28. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/02-diagnostico-funil.md +50 -50
  29. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/03-campanha-bidcap.md +68 -68
  30. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +44 -44
  31. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04a-criativos-ia.md +77 -77
  32. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04b-criativos-formatos.md +121 -121
  33. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/05-oferta.md +35 -35
  34. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06-pagina-vendas.md +122 -122
  35. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06a-instagram.md +93 -93
  36. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06b-player-vsl.md +57 -57
  37. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07-funil-upsell.md +83 -83
  38. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07a-copy-humanizacao.md +61 -61
  39. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/08-pesquisa-publico.md +11 -11
  40. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/09-especialista-coproducao.md +26 -26
  41. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/10-levantar-caixa.md +29 -29
  42. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/11-checklist-roi.md +22 -22
  43. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12-mineracao-ofertas.md +80 -80
  44. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12a-mineracao-veredito.md +72 -72
  45. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12b-fabrica-de-ofertas.md +63 -63
  46. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12c-anatomia-low-ticket.md +71 -71
  47. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/13-benchmarks-vsl.md +6 -6
  48. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/14-vocabulario.md +53 -51
  49. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/15-como-estender.md +26 -26
  50. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/17-rastreamento.md +76 -76
  51. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/18-copy-desejo-oferta.md +149 -149
  52. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/INDEX.md +45 -45
  53. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/guardrails.md +45 -45
  54. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +64 -66
  55. package/src/modules/lowticket/skills/wizz-lowticket-minerador/references/rubrica.md +63 -63
  56. package/src/modules/lowticket/skills/wizz-lowticket-pagina/references/metas-e-medicao.md +23 -23
  57. package/src/modules/lowticket/skills/wizz-lowticket-pagina/references/padroes-de-codigo.md +50 -50
  58. package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/formato-do-diagnostico.md +9 -9
  59. package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +42 -42
  60. package/src/modules/wizz/README.md +27 -27
  61. package/src/modules/wizz/_shared/cerebro.md +37 -37
  62. package/src/modules/wizz/_shared/communication-rules.md +15 -15
  63. package/src/modules/wizz/_shared/despacho-barato.md +6 -6
  64. package/src/modules/wizz/_shared/encerramento.md +23 -23
  65. package/src/modules/wizz/_shared/model-ladder.md +29 -28
  66. package/src/modules/wizz/_shared/planning-gate.md +30 -30
  67. package/src/modules/wizz/_shared/token-economy.md +28 -28
  68. package/src/modules/wizz/_shared/video-pipeline.md +42 -42
  69. package/src/modules/wizz/agents/wizz-ads/SKILL.md +16 -16
  70. package/src/modules/wizz/agents/wizz-ads/customize.toml +21 -21
  71. package/src/modules/wizz/agents/wizz-copy/SKILL.md +18 -18
  72. package/src/modules/wizz/agents/wizz-copy/customize.toml +24 -24
  73. package/src/modules/wizz/agents/wizz-designer/SKILL.md +24 -24
  74. package/src/modules/wizz/agents/wizz-designer/customize.toml +29 -29
  75. package/src/modules/wizz/agents/wizz-growth/SKILL.md +20 -20
  76. package/src/modules/wizz/agents/wizz-growth/customize.toml +34 -34
  77. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +63 -63
  78. package/src/modules/wizz/agents/wizz-maestro/customize.toml +38 -38
  79. package/src/modules/wizz/agents/wizz-memoria/SKILL.md +19 -19
  80. package/src/modules/wizz/agents/wizz-memoria/customize.toml +24 -24
  81. package/src/modules/wizz/agents/wizz-qa/SKILL.md +22 -22
  82. package/src/modules/wizz/agents/wizz-qa/customize.toml +29 -29
  83. package/src/modules/wizz/agents/wizz-seo/SKILL.md +19 -19
  84. package/src/modules/wizz/agents/wizz-seo/customize.toml +27 -27
  85. package/src/modules/wizz/agents/wizz-social/SKILL.md +53 -53
  86. package/src/modules/wizz/agents/wizz-social/customize.toml +37 -37
  87. package/src/modules/wizz/agents/wizz-social/knowledge/analise-imagem-referencia.md +20 -20
  88. package/src/modules/wizz/agents/wizz-social/knowledge/biblioteca-prompts-imagem.md +71 -71
  89. package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +196 -196
  90. package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +32 -32
  91. package/src/modules/wizz/agents/wizz-social/knowledge/prompts-imagem-video.md +48 -48
  92. package/src/modules/wizz/module.yaml +28 -28
  93. package/src/modules/wizz/overrides/wizz-agent-analyst.toml +17 -17
  94. package/src/modules/wizz/overrides/wizz-agent-architect.toml +21 -21
  95. package/src/modules/wizz/overrides/wizz-agent-dev.toml +20 -20
  96. package/src/modules/wizz/overrides/wizz-agent-pm.toml +11 -11
  97. package/src/modules/wizz/overrides/wizz-agent-tech-writer.toml +14 -14
  98. package/src/modules/wizz/overrides/wizz-agent-ux-designer.toml +14 -14
  99. package/src/modules/wizz/subagents/codex/wizz-exec-haiku.toml +17 -17
  100. package/src/modules/wizz/subagents/codex/wizz-exec-opus.toml +19 -19
  101. package/src/modules/wizz/subagents/codex/wizz-exec-review.toml +18 -17
  102. package/src/modules/wizz/subagents/codex/wizz-exec-sonnet.toml +18 -18
  103. package/src/modules/wizz/subagents/gemini/wizz-exec-haiku.md +17 -17
  104. package/src/modules/wizz/subagents/gemini/wizz-exec-opus.md +19 -19
  105. package/src/modules/wizz/subagents/gemini/wizz-exec-review.md +18 -17
  106. package/src/modules/wizz/subagents/gemini/wizz-exec-sonnet.md +18 -18
  107. package/src/modules/wizz/subagents/opencode/wizz-exec-haiku.md +17 -17
  108. package/src/modules/wizz/subagents/opencode/wizz-exec-opus.md +19 -19
  109. package/src/modules/wizz/subagents/opencode/wizz-exec-review.md +18 -17
  110. package/src/modules/wizz/subagents/opencode/wizz-exec-sonnet.md +18 -18
  111. package/src/modules/wizz/subagents/spec.yaml +84 -82
  112. package/src/modules/wizz/subagents/wizz-exec-haiku.md +17 -17
  113. package/src/modules/wizz/subagents/wizz-exec-opus.md +19 -19
  114. package/src/modules/wizz/subagents/wizz-exec-review.md +18 -17
  115. package/src/modules/wizz/subagents/wizz-exec-sonnet.md +18 -18
  116. package/src/skills-lib/cerebro/SKILL.md +61 -61
  117. package/src/skills-lib/cerebro/references/comandos-pesquisa-e-memoria.md +46 -46
  118. package/src/skills-lib/cerebro/references/comandos-rotina-e-captura.md +65 -65
  119. package/src/skills-lib/cerebro/references/comandos-sessao.md +74 -74
  120. package/src/skills-lib/ctc-align/SKILL.md +37 -37
  121. package/src/skills-lib/find-skills/SKILL.md +19 -19
  122. package/src/skills-lib/pesquisa-de-publico-do-piva/README.md +33 -33
  123. package/src/skills-lib/pesquisa-de-publico-do-piva/SKILL.md +42 -42
  124. package/src/skills-lib/pesquisa-de-publico-do-piva/prompts.md +20 -20
  125. package/src/skills-lib/references/premium-agency-rules.md +63 -63
  126. package/src/skills-lib/security-audit-pentest/SKILL.md +31 -31
  127. package/src/skills-lib/security-audit-pentest/references/stage-1-validacao-entrada.md +37 -37
  128. package/src/skills-lib/security-audit-pentest/references/stage-2-autorizacao.md +61 -61
  129. package/src/skills-lib/security-audit-pentest/references/stage-3-abuso-volume.md +37 -37
  130. package/src/skills-lib/security-audit-pentest/references/stage-4-vazamento-resposta.md +52 -52
  131. package/src/skills-lib/site-launch-kit/SKILL.md +42 -42
  132. package/src/skills-lib/site-launch-kit/references/01-cta-principal.md +20 -20
  133. package/src/skills-lib/site-launch-kit/references/02-barra-fixa-mobile.md +24 -24
  134. package/src/skills-lib/site-launch-kit/references/03-tempo-de-resposta.md +21 -21
  135. package/src/skills-lib/site-launch-kit/references/04-prova-social.md +22 -22
  136. package/src/skills-lib/site-launch-kit/references/05-faq-decisao.md +28 -28
  137. package/src/skills-lib/site-launch-kit/references/06-imagens-reais.md +27 -27
  138. package/src/skills-lib/site-launch-kit/references/07-endereco-como-chegar.md +25 -25
  139. package/src/skills-lib/site-launch-kit/references/08-titles.md +30 -30
  140. package/src/skills-lib/site-launch-kit/references/09-open-graph.md +23 -23
  141. package/src/skills-lib/site-launch-kit/references/10-breadcrumbs.md +23 -23
  142. package/src/skills-lib/site-launch-kit/references/11-alt-text.md +28 -28
  143. package/src/skills-lib/site-launch-kit/references/12-schema-local.md +22 -22
  144. package/src/skills-lib/site-launch-kit/references/13-robots-sitemap.md +26 -26
  145. package/src/skills-lib/site-launch-kit/references/14-privacidade-termos.md +27 -27
  146. package/src/skills-lib/site-launch-kit/references/15-medicao.md +23 -23
  147. package/src/skills-lib/wizz-ai-product-design/SKILL.md +70 -70
  148. package/src/skills-lib/wizz-auth-and-secrets/SKILL.md +78 -78
  149. package/src/skills-lib/wizz-caching-and-queues/SKILL.md +40 -40
  150. package/src/skills-lib/wizz-cloud-and-infra/SKILL.md +22 -22
  151. package/src/skills-lib/wizz-database-and-deps/SKILL.md +52 -52
  152. package/src/skills-lib/wizz-database-scaling/SKILL.md +31 -31
  153. package/src/skills-lib/wizz-desktop-security/SKILL.md +49 -49
  154. package/src/skills-lib/wizz-implementation-planner/SKILL.md +56 -56
  155. package/src/skills-lib/wizz-implementation-planner/references/folder-structure-and-dependencies.md +14 -14
  156. package/src/skills-lib/wizz-implementation-planner/references/implementation-phases.md +15 -15
  157. package/src/skills-lib/wizz-implementation-planner/references/implementation-types.md +9 -9
  158. package/src/skills-lib/wizz-implementation-planner/references/motion-path-implementation.md +19 -19
  159. package/src/skills-lib/wizz-implementation-planner/references/performance-plan.md +27 -27
  160. package/src/skills-lib/wizz-infrastructure/SKILL.md +21 -21
  161. package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +63 -63
  162. package/src/skills-lib/wizz-inicio-de-projeto/references/implementacao-api-forms.md +27 -27
  163. package/src/skills-lib/wizz-inicio-de-projeto/references/padroes-arquiteturais-e-pastas.md +31 -31
  164. package/src/skills-lib/wizz-inicio-de-projeto/references/seguranca-e-regras-codigo.md +21 -21
  165. package/src/skills-lib/wizz-launch-readiness/SKILL.md +61 -61
  166. package/src/skills-lib/wizz-launch-readiness/references/01-tecnico-build.md +16 -16
  167. package/src/skills-lib/wizz-launch-readiness/references/02-seguranca.md +16 -16
  168. package/src/skills-lib/wizz-launch-readiness/references/03-seo-descoberta.md +16 -16
  169. package/src/skills-lib/wizz-launch-readiness/references/04-analytics-medicao.md +15 -15
  170. package/src/skills-lib/wizz-launch-readiness/references/05-conteudo-prova-social.md +15 -15
  171. package/src/skills-lib/wizz-launch-readiness/references/06-legal-lgpd.md +15 -15
  172. package/src/skills-lib/wizz-launch-readiness/references/07-infra-deploy-rollback.md +16 -16
  173. package/src/skills-lib/wizz-launch-readiness/references/08-fusao-priorizacao.md +19 -19
  174. package/src/skills-lib/wizz-launch-readiness/references/09-persistencia-project-context.md +26 -26
  175. package/src/skills-lib/wizz-motion-3d-director/SKILL.md +70 -70
  176. package/src/skills-lib/wizz-motion-3d-director/references/image-to-video-workflow.md +34 -35
  177. package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +70 -70
  178. package/src/skills-lib/wizz-motion-3d-director/references/performance-and-handoff.md +17 -17
  179. package/src/skills-lib/wizz-offer-forge/README.md +9 -9
  180. package/src/skills-lib/wizz-offer-forge/SKILL.md +64 -64
  181. package/src/skills-lib/wizz-offer-forge/references/empilhamento-valor.md +17 -17
  182. package/src/skills-lib/wizz-offer-forge/references/garantia-reversa.md +16 -16
  183. package/src/skills-lib/wizz-offer-forge/references/mecanismo-unico.md +19 -19
  184. package/src/skills-lib/wizz-offer-forge/references/ponte-trafego.md +17 -17
  185. package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +36 -36
  186. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +81 -82
  187. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +105 -105
  188. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/core-goal.md +39 -39
  189. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/dashboard-and-portfolio-modes.md +49 -50
  190. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/handoffs.md +33 -34
  191. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/landing-page-strategy.md +141 -142
  192. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +24 -24
  193. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/output-format-and-quality.md +73 -73
  194. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/prompt-templates.md +4 -4
  195. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/site-levels.md +282 -283
  196. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +39 -39
  197. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-links.md +8 -8
  198. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/stack-and-visual-direction.md +63 -64
  199. package/src/skills-lib/wizz-router/SKILL.md +38 -38
  200. package/src/skills-lib/wizz-router/references/auditoria-360.md +12 -12
  201. package/src/skills-lib/wizz-router/references/routing-table-flat.md +80 -80
  202. package/src/skills-lib/wizz-tracking-audit/SKILL.md +48 -48
  203. package/src/skills-lib/wizz-tracking-audit/references/modo-diagnostico.md +43 -43
  204. package/src/skills-lib/wizz-tracking-audit/references/modo-implantacao.md +59 -59
  205. package/src/skills-lib/wizz-tracking-audit/references/utm-e-atribuicao.md +35 -35
  206. package/src/skills-lib/wizz-web-security/SKILL.md +35 -35
  207. package/src/skills-lib/wizz-web-security/references/checklist-deploy.md +50 -50
  208. package/src/skills-lib/wizz-web-security/references/headers-rate-limit-cors.md +33 -33
  209. package/src/skills-lib/wizz-web-security/references/owasp-top5-detalhado.md +36 -36
  210. package/tools/hooks/session-rules.js +14 -11
  211. package/tools/installer/core/resolved-customizations.js +58 -10
  212. package/tools/installer/core/resolved-schema.js +116 -18
@@ -1,10 +1,10 @@
1
- # Headers de Segurança, Rate Limiting, CORS, e Vazamento de Dados
1
+ # Security Headers, Rate Limiting, CORS, and Data Leakage
2
2
 
3
- Carregue este arquivo ao implementar/revisar headers de segurança, rate limiting, CORS, ou o formato de resposta de uma API (over-fetch/PII).
3
+ Load this file when implementing/reviewing security headers, rate limiting, CORS, or an API's response shape (over-fetch/PII).
4
4
 
5
- ## Headers de segurança obrigatórios
5
+ ## Mandatory security headers
6
6
 
7
- `X-Frame-Options: DENY` + `frame-ancestors 'none'` no CSP mata **clickjacking** (o site não pode ser embutido em `<iframe>` de outro domínio). Fix de 1 minuto, sempre presente.
7
+ `X-Frame-Options: DENY` + `frame-ancestors 'none'` in the CSP kills **clickjacking** (the site can't be embedded in an `<iframe>` from another domain). A 1-minute fix, always present.
8
8
 
9
9
  ```
10
10
  Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
@@ -15,7 +15,7 @@ Referrer-Policy: strict-origin-when-cross-origin
15
15
  Permissions-Policy: camera=(), microphone=(), geolocation=()
16
16
  ```
17
17
 
18
- Como adicionar em Next.js (`next.config.ts`):
18
+ How to add it in Next.js (`next.config.ts`):
19
19
  ```ts
20
20
  async headers() {
21
21
  return [
@@ -44,46 +44,46 @@ async headers() {
44
44
  },
45
45
  ```
46
46
 
47
- > **Cuidado:** CSP errada quebra o app. Teste sempre em dev antes de subir. Ajuste os domínios (`script-src`, `connect-src`) conforme os serviços reais do projeto.
47
+ > **Careful:** a wrong CSP breaks the app. Always test in dev before deploying. Adjust the domains (`script-src`, `connect-src`) to the project's real services.
48
48
 
49
49
  ## Rate Limiting
50
- - Login: limite por IP **e** por conta (só por IP não segura brute force distribuído; só por conta não segura scan de vários e-mails)
51
- - APIs públicas: limite por IP e por token
52
- - Endpoints de reset de senha: especialmente restritivos
53
- - Endpoints de cobrança (checkout, portal): sempre com rate limit
54
- - Next.js: `@upstash/ratelimit` no route handler, ou WAF da Vercel para regras por rota
55
- - Fail closed: se o backend do rate limit cair (ex: Redis fora), rotas de escrita devem negar a request, não deixar passar
56
- - Comparações de token/secret: `crypto.timingSafeEqual`, nunca `!==` (detalhe em wizz-auth-and-secrets)
57
- - Sem rate limit = brute force de graça. Somar lockout/CAPTCHA após N falhas no login
50
+ - Login: limit per IP **and** per account (IP-only doesn't protect against distributed brute force; account-only doesn't protect against scanning many emails)
51
+ - Public APIs: limit per IP and per token
52
+ - Password reset endpoints: especially restrictive
53
+ - Billing endpoints (checkout, portal): always with rate limiting
54
+ - Next.js: `@upstash/ratelimit` in the route handler, or Vercel's WAF for per-route rules
55
+ - Fail closed: if the rate-limit backend goes down (e.g. Redis is out), write routes should deny the request, not let it through
56
+ - Token/secret comparisons: `crypto.timingSafeEqual`, never `!==` (details in wizz-auth-and-secrets)
57
+ - No rate limit = free brute force. Add lockout/CAPTCHA after N failed login attempts
58
58
 
59
- ### Origem do IP atrás de proxy/CDN
60
- Se o app roda atrás de Vercel/Cloudflare/qualquer proxy, o IP do socket é o do proxy, não o do cliente. Ler o socket direto faz **o mundo inteiro contar como um IP só** e o rate limit deixa de existir na prática.
61
- - Vercel: use `x-forwarded-for` (ou `req.ip` no runtime que já resolve). Pegue o **primeiro** IP da lista, mas só confie na cadeia que vem do seu proxy.
62
- - Nunca confie em `x-forwarded-for` cru vindo de origem não-proxy (o cliente falsifica o header). Configure a lib de rate limit para o número certo de proxies à frente.
59
+ ### IP origin behind a proxy/CDN
60
+ If the app runs behind Vercel/Cloudflare/any proxy, the socket's IP is the proxy's, not the client's. Reading the raw socket makes **the entire world count as a single IP** and the rate limit effectively stops existing.
61
+ - Vercel: use `x-forwarded-for` (or `req.ip` in the runtime that already resolves it). Take the **first** IP in the list, but only trust the chain that comes from your own proxy.
62
+ - Never trust a raw `x-forwarded-for` coming from a non-proxy origin (the client can spoof the header). Configure the rate-limit library for the correct number of proxies in front.
63
63
 
64
- ### Custo por chamada (rota que gasta dinheiro real)
65
- Toda rota que chama modelo de linguagem, dispara SMS, envia e-mail, processa arquivo ou bate em API paga de terceiro precisa de rate limit **e** quota por conta. Rota de agente de IA exposta sem limitador é crítica mesmo sem vazar dado nenhum: 10.000 chamadas em loop viram conta de centenas a milhares de reais/dólares. Estime o custo unitário e multiplique por 10k ao avaliar a rota.
64
+ ### Per-call cost (a route that spends real money)
65
+ Every route that calls a language model, sends an SMS, sends an email, processes a file, or hits a paid third-party API needs rate limiting **and** a per-account quota. An AI agent route exposed without a limiter is critical even without leaking any data at all: 10,000 calls in a loop turn into a bill of hundreds to thousands of dollars. Estimate the unit cost and multiply by 10k when assessing the route.
66
66
 
67
- ### Extração por paginação
68
- Rota que devolve um registro por vez e, chamada em sequência, entrega a base inteira. Barre com: filtro por dono na query (o mesmo do IDOR), limite máximo de página, e detecção de varredura (muitos ids sequenciais do mesmo cliente).
67
+ ### Extraction via pagination
68
+ A route that returns one record at a time and, called in sequence, hands over the entire database. Block it with: an owner filter in the query (the same as for IDOR), a maximum page size, and scan detection (many sequential IDs from the same client).
69
69
 
70
70
  ## CORS
71
- - **Nunca reflita o header `Origin` recebido** de volta em `Access-Control-Allow-Origin` (isso libera qualquer site a chamar sua API com credenciais)
72
- - Use uma **allowlist explícita** de origens: compare o `Origin` recebido contra a lista e só então ecoe o valor exato daquela origem
73
- - `Access-Control-Allow-Credentials: true` NUNCA junto de `Access-Control-Allow-Origin: *`
74
- - Padrão = negar: origem desconhecida → sem headers CORS (o browser bloqueia sozinho)
71
+ - **Never reflect the received `Origin` header** back in `Access-Control-Allow-Origin` (this lets any site call your API with credentials)
72
+ - Use an explicit **allowlist** of origins: compare the received `Origin` against the list and only then echo back that exact origin's value
73
+ - `Access-Control-Allow-Credentials: true` NEVER together with `Access-Control-Allow-Origin: *`
74
+ - Default = deny: unknown origin → no CORS headers (the browser blocks it on its own)
75
75
  ```ts
76
76
  const ALLOWED = new Set(["https://app.exemplo.com", "https://exemplo.com"])
77
77
  const origin = req.headers.get("origin")
78
78
  if (origin && ALLOWED.has(origin)) {
79
- res.headers.set("Access-Control-Allow-Origin", origin) // valor exato, nunca "*" com credentials
79
+ res.headers.set("Access-Control-Allow-Origin", origin) // exact value, never "*" with credentials
80
80
  res.headers.set("Vary", "Origin")
81
81
  }
82
82
  ```
83
83
 
84
- ## Vazamento de dados na resposta (over-fetch / PII)
85
- - Retorne **só os campos que a tela precisa**: monte um DTO/allowlist de saída, nunca devolva a row inteira do banco
86
- - NUNCA inclua `password`, `password_hash`, tokens, secrets ou colunas internas na resposta de API (é a falha mais grave da lista de PII)
87
- - Supabase: `.select('id, name, email')` explícito, nunca `.select('*')` em endpoint que responde ao cliente
88
- - Objeto do usuário logado: serialize por um mapper que só expõe campos públicos
89
- - Erros: não vaze stack trace, SQL nem nome de tabela pro cliente. Mensagem genérica no response, detalhe só no log do servidor
84
+ ## Data leakage in the response (over-fetch / PII)
85
+ - Return **only the fields the screen needs**: build an output DTO/allowlist, never return the whole database row
86
+ - NEVER include `password`, `password_hash`, tokens, secrets, or internal columns in an API response (this is the most serious failure on the PII list)
87
+ - Supabase: explicit `.select('id, name, email')`, never `.select('*')` on an endpoint that responds to the client
88
+ - Logged-in user object: serialize it through a mapper that only exposes public fields
89
+ - Errors: don't leak stack traces, SQL, or table names to the client. Generic message in the response, detail only in the server log
@@ -1,14 +1,14 @@
1
- # OWASP — Detalhe das 5 categorias mais comuns
1
+ # OWASP — Detail of the 5 most common categories
2
2
 
3
- Carregue este arquivo ao aprofundar numa das falhas do triage rápido (seções 1-5 referenciadas na tabela do SKILL.md).
3
+ Load this file when digging deeper into one of the failures from the quick triage (sections 1-5 referenced in the SKILL.md table).
4
4
 
5
5
  ## 1. Broken Access Control
6
- - Verifique permissões no servidor, nunca apenas no frontend
7
- - IDOR (Insecure Direct Object Reference): valide se o usuário tem acesso ao recurso pelo ID
8
- - Princípio do menor privilégio em todas as rotas
9
- - Next.js: middleware não é a única barreira. Re-verifique auth dentro do route handler, Server Action ou página (bypass de middleware já teve CVE)
6
+ - Check permissions on the server, never only on the frontend
7
+ - IDOR (Insecure Direct Object Reference): validate that the user has access to the resource by ID
8
+ - Principle of least privilege on every route
9
+ - Next.js: middleware isn't the only barrier. Re-check auth inside the route handler, Server Action, or page (a middleware bypass has already had a CVE)
10
10
 
11
- Exemplo anti-IDOR (Next.js + Clerk + Supabase):
11
+ Anti-IDOR example (Next.js + Clerk + Supabase):
12
12
  ```ts
13
13
  // app/api/projects/[id]/route.ts
14
14
  import { auth } from "@clerk/nextjs/server"
@@ -21,63 +21,63 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
21
21
  .from("projects")
22
22
  .select("id, name")
23
23
  .eq("id", id)
24
- .eq("user_id", userId) // posse verificada na query, além do RLS
24
+ .eq("user_id", userId) // ownership verified in the query, in addition to RLS
25
25
  .single()
26
- if (!data) return new Response("Not found", { status: 404 }) // 404, não 403 (não confirma que o recurso existe)
26
+ if (!data) return new Response("Not found", { status: 404 }) // 404, not 403 (doesn't confirm the resource exists)
27
27
  return Response.json(data)
28
28
  }
29
29
  ```
30
30
 
31
- ### RLS: policy permissiva é pior que policy nenhuma
32
- Ler os arquivos de migration/policy versionados e checar:
33
- - Toda tabela de domínio tem `ENABLE ROW LEVEL SECURITY`?
34
- - Nenhuma policy é permissiva: `USING (true)` ou `WITH CHECK (true)` libera geral, mas o painel do Supabase mostra a trava como "ativa". Amarre sempre ao dono: `USING (auth.uid() = user_id)`.
35
- - Tabela criada em migration recente **sem** policy correspondente = exposta.
31
+ ### RLS: a permissive policy is worse than no policy
32
+ Read the versioned migration/policy files and check:
33
+ - Does every domain table have `ENABLE ROW LEVEL SECURITY`?
34
+ - Is no policy permissive: `USING (true)` or `WITH CHECK (true)` opens it to everyone, but the Supabase dashboard shows the lock as "active." Always tie it to the owner: `USING (auth.uid() = user_id)`.
35
+ - A table created in a recent migration **without** a corresponding policy = exposed.
36
36
 
37
37
  ```sql
38
- -- ERRADO: mostra RLS ativo no painel, mas libera qualquer um
38
+ -- WRONG: shows RLS active in the dashboard, but opens it to anyone
39
39
  create policy "read" on projects for select using (true);
40
40
 
41
- -- CERTO: amarra o dono
41
+ -- RIGHT: ties it to the owner
42
42
  alter table projects enable row level security;
43
43
  create policy "own rows" on projects
44
44
  for select using (auth.uid() = user_id);
45
45
  ```
46
46
 
47
- > RLS de verdade é estado do banco, não arquivo. O que está no repo é ponto de partida: confirme o estado real no dashboard/`\d+` do Postgres.
47
+ > Real RLS is database state, not a file. What's in the repo is a starting point: confirm the real state in the dashboard/`\d+` of Postgres.
48
48
 
49
- ## 6. Mass Assignment (atribuição em massa)
50
- O corpo da requisição inteiro repassado para o banco (`spread` do objeto, `update`/`create` com o payload completo) deixa o atacante setar qualquer coluna, mesmo as que não estão no formulário. Alvos clássicos: `role`, `is_admin`, `plan`, `balance`, `payment_status`, `user_id`/`owner_id`.
49
+ ## 6. Mass Assignment
50
+ Passing the entire request body through to the database (`spread` of the object, `update`/`create` with the full payload) lets an attacker set any column, even ones not in the form. Classic targets: `role`, `is_admin`, `plan`, `balance`, `payment_status`, `user_id`/`owner_id`.
51
51
 
52
52
  ```ts
53
- // ERRADO: o cliente manda { name: "x", role: "admin" } e vira admin
53
+ // WRONG: the client sends { name: "x", role: "admin" } and becomes admin
54
54
  await supabase.from("users").update({ ...body }).eq("id", userId)
55
55
 
56
- // CERTO: allowlist explícita, só os campos do formulário
57
- const { name, bio } = updateSchema.parse(body) // Zod com só os campos permitidos
56
+ // RIGHT: explicit allowlist, only the form's fields
57
+ const { name, bio } = updateSchema.parse(body) // Zod with only the allowed fields
58
58
  await supabase.from("users").update({ name, bio }).eq("id", userId)
59
59
  ```
60
60
 
61
- Regra: **nunca** `...body` / `...req.body` num write. Sempre desestruture (ou valide com um schema que só contém os campos editáveis). Campos de papel, permissão, plano, saldo, status de pagamento e id de dono nunca vêm do cliente.
61
+ Rule: **never** `...body` / `...req.body` in a write. Always destructure (or validate with a schema that only contains the editable fields). Role, permission, plan, balance, payment status, and owner id fields never come from the client.
62
62
 
63
63
  ## 2. Cryptographic Failures
64
- - HTTPS em tudo, sem exceção
65
- - HSTS header com includeSubDomains
66
- - Não exponha dados sensíveis em URLs (query strings ficam em logs)
64
+ - HTTPS everywhere, no exceptions
65
+ - HSTS header with includeSubDomains
66
+ - Don't expose sensitive data in URLs (query strings end up in logs)
67
67
 
68
68
  ## 3. Injection
69
- - SQL: queries parametrizadas sempre
69
+ - SQL: always parameterized queries
70
70
  - XSS: sanitize outputs, use Content-Security-Policy
71
- - Command injection: nunca execute input do usuário como comando shell
71
+ - Command injection: never execute user input as a shell command
72
72
 
73
73
  ## 4. Cross-Site Scripting (XSS)
74
- - Escape HTML em todo output de dado do usuário
75
- - React/Next.js escapa JSX por padrão. O risco real está em `dangerouslySetInnerHTML`: sanitize antes com DOMPurify (ou não use)
76
- - URLs vindas do usuário em `href`/`src`: bloqueie esquemas `javascript:` e `data:` (valide que começa com `https://` ou caminho relativo)
77
- - Content-Security-Policy: bloqueie inline scripts
78
- - HttpOnly e Secure flags nos cookies de sessão
74
+ - Escape HTML on every piece of user-generated output
75
+ - React/Next.js escapes JSX by default. The real risk is in `dangerouslySetInnerHTML`: sanitize with DOMPurify first (or don't use it)
76
+ - User-supplied URLs in `href`/`src`: block `javascript:` and `data:` schemes (validate that it starts with `https://` or a relative path)
77
+ - Content-Security-Policy: block inline scripts
78
+ - HttpOnly and Secure flags on session cookies
79
79
 
80
80
  ## 5. CSRF
81
- - SameSite=Strict ou Lax nos cookies de sessão
82
- - CSRF token em formulários state-changing
83
- - Verifique Origin/Referer em requests sensíveis
81
+ - SameSite=Strict or Lax on session cookies
82
+ - CSRF token on state-changing forms
83
+ - Check Origin/Referer on sensitive requests
@@ -4,9 +4,9 @@
4
4
  //
5
5
  // Três responsabilidades, um processo só (SessionStart roda 1x por sessão):
6
6
  //
7
- // 1. Regras de comunicação, injetadas UMA vez por sessão (antes:
8
- // no-narration-enforce repetia ~80 tokens em TODO prompt via
9
- // UserPromptSubmit). Mesmo conteúdo, custo fixo por sessão.
7
+ // 1. Communication rules, injected ONCE per session (before: no-narration-enforce
8
+ // repeated ~80 tokens on EVERY prompt via UserPromptSubmit). Same content,
9
+ // fixed cost per session.
10
10
  //
11
11
  // 2. Feature ativa: resolve o `active_feature` SOZINHO, sem o usuário
12
12
  // precisar invocar `wizz-set-feature` e responder duas perguntas. Ordem:
@@ -26,15 +26,18 @@ const fs = require('node:fs');
26
26
  const os = require('node:os');
27
27
  const path = require('node:path');
28
28
 
29
- // Cópia comprimida e INTENCIONAL da regra de comunicação. A fonte é
30
- // src/modules/wizz/_shared/communication-rules.md; este hook precisa ser
31
- // autocontido porque roda de ~/.claude/hooks, longe do repo. Editou lá,
32
- // edite aqui: test/test-feature-context.js falha se as duas divergirem.
29
+ // Compressed and INTENTIONAL copy of the communication rule. The source is
30
+ // src/modules/wizz/_shared/communication-rules.md; this hook needs to be
31
+ // self-contained because it runs from ~/.claude/hooks, far from the repo. Edit
32
+ // it there, edit it here too: test/test-feature-context.js fails if the two diverge.
33
33
  const RULES =
34
- 'REGRA DE COMUNICAÇÃO (permanente, não-negociável): não narrar o passo a passo ' +
35
- 'enquanto trabalha; pausar e perguntar SÓ em decisão do usuário ou risco ' +
36
- 'irreversível; fechar SEMPRE com resumo curto em tópicos, linguagem simples, ' +
37
- 'sem jargão; sem em-dashes; comunicação enxuta por default, expandir só sob demanda.';
34
+ 'COMMUNICATION RULE (permanent, non-negotiable): do not narrate the step-by-step ' +
35
+ 'while working; pause and ask ONLY for a user decision or irreversible risk; ' +
36
+ 'always close with a short summary in bullet points, plain language, no jargon; ' +
37
+ 'no em-dashes only in text a third party reads (public page, copy, UI), ' +
38
+ 'code comments, commits, and internal notes are exempt from this check; ' +
39
+ 'concise communication by default, expand only on request; ' +
40
+ 'reply to the user in Brazilian Portuguese.';
38
41
 
39
42
  // Branches que NUNCA viram feature: são linha principal, não trabalho nomeado.
40
43
  const TRUNK = new Set(['main', 'master', 'develop', 'dev', 'staging', 'stage', 'prod', 'production', 'trunk', 'default', 'release']);
@@ -78,11 +78,35 @@ function detectKeyedMergeField(items) {
78
78
  return null;
79
79
  }
80
80
 
81
+ /**
82
+ * Warn when a team/user override REPLACES a base review layer (matched by
83
+ * `id`/`code`) but omits `tiers` or `max_calls` the base layer had.
84
+ * `mergeByKey` replaces the whole matched item — it never merges an
85
+ * override item field-by-field against its base counterpart (Python
86
+ * parity) — so dropping those fields silently loses the base layer's
87
+ * scaling: the layer now runs on every tier and/or with no call cap.
88
+ * No-op when `opts.warnings`/`opts.layerLabel` are not supplied, so callers
89
+ * that don't care about this (e.g. direct `deepMerge` calls in tests) pay
90
+ * nothing for it.
91
+ */
92
+ function warnOverrideDropsLayerScaling({ warnings, layerLabel, key, matchedBase, item }) {
93
+ if (!warnings || !layerLabel) return;
94
+ const missing = ['tiers', 'max_calls'].filter((field) => hasOwn(matchedBase, field) && !hasOwn(item, field));
95
+ if (missing.length === 0) return;
96
+ const consequence = missing.map((field) => (field === 'tiers' ? 'every tier' : 'uncapped')).join(' and ');
97
+ warnings.push(
98
+ `${layerLabel} override: "${key}" replaces the whole base layer and omits ${missing.join(' and ')} — it now runs on ${consequence}`,
99
+ );
100
+ }
101
+
81
102
  /**
82
103
  * Keyed merge: matching identifiers replace in place, new ones append.
83
104
  * Non-table items in the override are appended (Python parity).
105
+ * @param {object} [opts] - `{ warnings, layerLabel }` to collect the
106
+ * dropped-scaling warning above; omit for warning-free merges (tests,
107
+ * internal recomputation).
84
108
  */
85
- function mergeByKey(base, override, keyName) {
109
+ function mergeByKey(base, override, keyName, opts) {
86
110
  const result = [];
87
111
  const indexByKey = new Map();
88
112
  for (const item of base) {
@@ -99,7 +123,9 @@ function mergeByKey(base, override, keyName) {
99
123
  }
100
124
  const key = item[keyName];
101
125
  if (key !== undefined && key !== null && indexByKey.has(key)) {
102
- result[indexByKey.get(key)] = { ...item };
126
+ const matchedIndex = indexByKey.get(key);
127
+ if (opts) warnOverrideDropsLayerScaling({ ...opts, key, matchedBase: result[matchedIndex], item });
128
+ result[matchedIndex] = { ...item };
103
129
  } else {
104
130
  if (key !== undefined && key !== null) indexByKey.set(key, result.length);
105
131
  result.push({ ...item });
@@ -112,30 +138,33 @@ function mergeByKey(base, override, keyName) {
112
138
  * Shape-aware array merge: keyed merge when every item (base + override)
113
139
  * shares `code` or `id`, else append.
114
140
  */
115
- function mergeArrays(base, override) {
141
+ function mergeArrays(base, override, opts) {
116
142
  const baseArr = Array.isArray(base) ? base : [];
117
143
  const overrideArr = Array.isArray(override) ? override : [];
118
144
  const keyedField = detectKeyedMergeField([...baseArr, ...overrideArr]);
119
- if (keyedField) return mergeByKey(baseArr, overrideArr, keyedField);
145
+ if (keyedField) return mergeByKey(baseArr, overrideArr, keyedField, opts);
120
146
  return [...baseArr, ...overrideArr];
121
147
  }
122
148
 
123
149
  /**
124
150
  * Structural deep merge. Tables deep-merge, arrays shape-merge,
125
151
  * everything else: override wins. Python `deep_merge` parity.
152
+ * @param {object} [opts] - `{ warnings, layerLabel }`, threaded down to
153
+ * every `mergeByKey` call reached while walking `base`/`override`; see
154
+ * `warnOverrideDropsLayerScaling`.
126
155
  */
127
- function deepMerge(base, override) {
156
+ function deepMerge(base, override, opts) {
128
157
  if (isPlainObject(base) && isPlainObject(override)) {
129
158
  const result = { ...base };
130
159
  for (const [key, overVal] of Object.entries(override)) {
131
160
  // hasOwn, not `in`: `'constructor' in result` is true via the
132
161
  // prototype chain even when the base layer never declared it, which
133
162
  // would deep-merge the override into Object's constructor function.
134
- safeSet(result, key, hasOwn(result, key) ? deepMerge(result[key], overVal) : overVal);
163
+ safeSet(result, key, hasOwn(result, key) ? deepMerge(result[key], overVal, opts) : overVal);
135
164
  }
136
165
  return result;
137
166
  }
138
- if (Array.isArray(base) && Array.isArray(override)) return mergeArrays(base, override);
167
+ if (Array.isArray(base) && Array.isArray(override)) return mergeArrays(base, override, opts);
139
168
  return override;
140
169
  }
141
170
 
@@ -455,16 +484,35 @@ async function resolveSkillSnapshot({ customizePath, customDir, resolvedDir, pro
455
484
  // layer is exempt (it owns the LLM-only fields and new experimental
456
485
  // keys); a typo here is exactly the silent-merge the audit flagged.
457
486
  // Warnings, never errors — the snapshot is still written (fail-open).
487
+ // findUnknownKeys() returns structured { path, kind, detail? } entries:
488
+ // `kind` says "unknown key" vs "invalid value" directly, so this loop
489
+ // never has to sniff the rendered path text (a quoted TOML key can itself
490
+ // contain a space, which broke the old `includes(' ')` heuristic).
458
491
  for (const [layerName, layer] of [
459
492
  ['team', teamRaw],
460
493
  ['user', userRaw],
461
494
  ]) {
462
- for (const unknown of findUnknownKeys(layer)) {
463
- warnings.push(`${layerName} override: unknown key "${unknown}" (not a recognized workflow field — typo? check customize.toml docs)`);
495
+ for (const { path: keyPath, kind, detail } of findUnknownKeys(layer)) {
496
+ warnings.push(
497
+ kind === 'invalid'
498
+ ? `${layerName} override: invalid ${keyPath}${detail ? ` (${detail})` : ''} — check customize.toml docs`
499
+ : `${layerName} override: unknown key "${keyPath}" (not a recognized workflow field — typo? check customize.toml docs)`,
500
+ );
464
501
  }
465
502
  }
466
503
 
467
- const merged = deepMerge(deepMerge(defaults, team), user);
504
+ // Audit item 24 follow-up: a team/user override that REPLACES a base
505
+ // review layer (matched by id/code) but drops the base's `tiers`/
506
+ // `max_calls` silently loses that scaling — see
507
+ // warnOverrideDropsLayerScaling. Two separate deepMerge calls (rather
508
+ // than the previous deepMerge(deepMerge(defaults, team), user)) so each
509
+ // one can be labeled with the layer that is doing the overriding; a user
510
+ // override is checked against the team-adjusted result, which is the
511
+ // layer it actually replaces.
512
+ const overrideWarnings = [];
513
+ const afterTeam = deepMerge(defaults, team, { warnings: overrideWarnings, layerLabel: 'team' });
514
+ const merged = deepMerge(afterTeam, user, { warnings: overrideWarnings, layerLabel: 'user' });
515
+ warnings.push(...overrideWarnings);
468
516
  const body = `${JSON.stringify(merged, null, 2)}\n`;
469
517
  // Atomic write via fs-native (tmp + rename), consistent with every other
470
518
  // config file the installer writes.
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Extracted from `resolved-customizations.js` (2026-09-11) to keep that file
5
5
  * under the 800-line cap. Validation only: nothing here writes, parses or
6
- * merges — it takes ONE parsed layer and reports the keys no code consumes.
6
+ * merges — it takes ONE parsed layer and reports the keys/values no code
7
+ * consumes or that are structurally invalid.
7
8
  *
8
9
  * "additionalProperties: false" in spirit. The BASE layer (shipped,
9
10
  * code-reviewed) is exempt: it may legitimately carry LLM-only fields
@@ -14,6 +15,12 @@
14
15
  * is a warning (not an error: fail-open, the snapshot is still written).
15
16
  * `include` is the documented extension point and is validated by
16
17
  * `resolveIncludes`.
18
+ *
19
+ * Findings are structured `{ path, kind: 'unknown' | 'invalid', detail? }`
20
+ * entries — never plain strings. `kind` is how a caller tells "this key
21
+ * doesn't exist" from "this key exists but its value is wrong"; sniffing the
22
+ * rendered path text (e.g. for a space) breaks the moment a dotted path or a
23
+ * quoted TOML key legitimately contains one.
17
24
  */
18
25
 
19
26
  const { isPlainObject } = require('./resolved-includes');
@@ -72,31 +79,56 @@ const WORKFLOW_KEYS = new Set([
72
79
  // item keys are validated against REVIEW_LAYER_KEYS below.
73
80
  'review_layers',
74
81
  'oneshot_review_layers',
82
+ // review economy (review-economy.md): diff cleaning excludes/critical paths.
83
+ 'review_exclude',
84
+ 'review_critical_paths',
75
85
  ]);
76
86
  const PARTY_MEMBER_KEYS = new Set(['code', 'name', 'icon', 'title', 'persona', 'capabilities', 'model']);
77
87
  const PARTY_GROUP_KEYS = new Set(['id', 'name', 'scene', 'members', 'memory']);
78
- // Exactly the keys the 8 shipped `[[workflow.*review_layers]]` entries use
79
- // (src/bmm-skills/4-implementation/wizz-code-review + wizz-quick-dev).
80
- const REVIEW_LAYER_KEYS = new Set(['id', 'name', 'instruction', 'when']);
88
+ // Exactly the keys the shipped `[[workflow.*review_layers]]` entries use
89
+ // (src/bmm-skills/4-implementation/wizz-code-review + wizz-quick-dev), plus
90
+ // `tiers`/`max_calls` for the review-economy tier scaling (review-economy.md).
91
+ const REVIEW_LAYER_KEYS = new Set(['id', 'name', 'instruction', 'when', 'tiers', 'max_calls']);
92
+ // The only valid values for a review_layers/oneshot_review_layers `tiers`
93
+ // entry (review-economy.md tier scaling). A typo (`"crtical"`) used to load
94
+ // silently and drop the layer on every tier — nothing ever matched it.
95
+ const VALID_REVIEW_TIERS = new Set(['small', 'normal', 'critical']);
81
96
  // The only top-level keys any of the 47 shipped customize.toml carry. A
82
97
  // top-level typo (`[workflw]`) or a stray scalar merged into silence before.
83
98
  const TOP_LEVEL_KEYS = new Set(['workflow', 'agent', 'include']);
84
99
 
100
+ /** `value` is an array whose every element is a string. */
101
+ function isArrayOfStrings(value) {
102
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
103
+ }
104
+
85
105
  /**
86
- * Collect unknown keys under `[workflow]`: unknown fields by name, and for
87
- * the arrays-of-tables we own (party members/groups, review layers) the
88
- * unknown keys of each item.
89
- * @returns {string[]} dotted paths
106
+ * Collect unknown/invalid entries under `[workflow]`: unknown fields by
107
+ * name, invalid values for the two review-economy string-array fields, and
108
+ * — for the arrays-of-tables we own (party members/groups, review layers) —
109
+ * the unknown keys and invalid values of each item.
110
+ *
111
+ * Every array-of-tables item is validated on its own, even when the array
112
+ * also holds non-object elements: a non-object item is flagged (invalid)
113
+ * instead of skipping validation for the whole array, which used to let a
114
+ * stray non-table entry hide typos in every OTHER entry.
115
+ * @returns {Array<{path: string, kind: 'unknown'|'invalid', detail?: string}>}
90
116
  */
91
117
  function findUnknownWorkflowKeys(workflow) {
92
118
  const unknown = [];
93
119
  for (const [k2, v2] of Object.entries(workflow)) {
94
120
  const wfPath = `workflow.${k2}`;
95
121
  if (!WORKFLOW_KEYS.has(k2)) {
96
- unknown.push(wfPath);
122
+ unknown.push({ path: wfPath, kind: 'unknown' });
97
123
  continue;
98
124
  }
99
- if (!Array.isArray(v2) || v2.length === 0 || !v2.every((it) => isPlainObject(it))) continue;
125
+ if (k2 === 'review_exclude' || k2 === 'review_critical_paths') {
126
+ if (!isArrayOfStrings(v2)) {
127
+ unknown.push({ path: wfPath, kind: 'invalid', detail: `expected an array of strings, got ${JSON.stringify(v2)}` });
128
+ }
129
+ continue;
130
+ }
131
+ if (!Array.isArray(v2) || v2.length === 0) continue;
100
132
  const allowed =
101
133
  k2 === 'party_members'
102
134
  ? PARTY_MEMBER_KEYS
@@ -106,30 +138,93 @@ function findUnknownWorkflowKeys(workflow) {
106
138
  ? REVIEW_LAYER_KEYS
107
139
  : null;
108
140
  if (!allowed) continue;
141
+ const isOneshot = k2 === 'oneshot_review_layers';
109
142
  for (const item of v2) {
143
+ if (!isPlainObject(item)) {
144
+ unknown.push({ path: `${wfPath}[]`, kind: 'invalid', detail: `expected a table, got ${JSON.stringify(item)}` });
145
+ continue;
146
+ }
110
147
  for (const k3 of Object.keys(item)) {
111
- if (!allowed.has(k3)) unknown.push(`${wfPath}[].${k3}`);
148
+ if (!allowed.has(k3)) unknown.push({ path: `${wfPath}[].${k3}`, kind: 'unknown' });
112
149
  }
150
+ if (allowed === REVIEW_LAYER_KEYS) unknown.push(...findInvalidReviewLayerValues(item, wfPath, isOneshot));
113
151
  }
114
152
  }
115
153
  return unknown;
116
154
  }
117
155
 
118
156
  /**
119
- * Walk a loaded TOML layer and collect unknown keys: unknown TOP-level
120
- * tables/scalars (anything outside `workflow`, `agent`, `include`) plus the
121
- * `[workflow]` fields and array-of-table item keys. `[agent]` is prose the
122
- * LLM reads, not a code-consumed schema, so its keys are open by design;
123
- * `include` is validated by resolveIncludes.
157
+ * Validate the VALUES of a review_layers/oneshot_review_layers item (item
158
+ * keys themselves are already checked by the caller against
159
+ * REVIEW_LAYER_KEYS). A typo in `tiers` (`"crtical"`), an empty `tiers`
160
+ * array, a non-positive `max_calls`, or a `tiers` entry on an
161
+ * `oneshot_review_layers` item (that step ignores tiers — it always runs)
162
+ * all load silently today: deepMerge applies them, nothing catches the
163
+ * mistake, the layer misbehaves. Warnings, same shape as unknown keys —
164
+ * fail-open, the snapshot is still written.
165
+ * @param {object} item
166
+ * @param {string} wfPath
167
+ * @param {boolean} isOneshot - true for an `oneshot_review_layers` item
168
+ * @returns {Array<{path: string, kind: 'invalid', detail: string}>}
169
+ */
170
+ function findInvalidReviewLayerValues(item, wfPath, isOneshot) {
171
+ const invalid = [];
172
+ if ('tiers' in item) {
173
+ if (isOneshot) {
174
+ invalid.push({
175
+ path: `${wfPath}[].tiers`,
176
+ kind: 'invalid',
177
+ detail: 'oneshot_review_layers always runs regardless of tier — tiers has no effect here',
178
+ });
179
+ } else if (!Array.isArray(item.tiers)) {
180
+ invalid.push({
181
+ path: `${wfPath}[].tiers`,
182
+ kind: 'invalid',
183
+ detail: `expected an array of "small"/"normal"/"critical", got ${JSON.stringify(item.tiers)}`,
184
+ });
185
+ } else if (item.tiers.length === 0) {
186
+ invalid.push({
187
+ path: `${wfPath}[].tiers`,
188
+ kind: 'invalid',
189
+ detail: 'empty — the layer would never run on any tier',
190
+ });
191
+ } else {
192
+ for (const tier of item.tiers) {
193
+ if (!VALID_REVIEW_TIERS.has(tier)) {
194
+ invalid.push({
195
+ path: `${wfPath}[].tiers[]`,
196
+ kind: 'invalid',
197
+ detail: `invalid tier "${tier}" — expected one of "small", "normal", "critical"`,
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ if ('max_calls' in item && !(Number.isInteger(item.max_calls) && item.max_calls > 0)) {
204
+ invalid.push({
205
+ path: `${wfPath}[].max_calls`,
206
+ kind: 'invalid',
207
+ detail: `expected a positive integer, got ${JSON.stringify(item.max_calls)}`,
208
+ });
209
+ }
210
+ return invalid;
211
+ }
212
+
213
+ /**
214
+ * Walk a loaded TOML layer and collect unknown/invalid entries: unknown
215
+ * TOP-level tables/scalars (anything outside `workflow`, `agent`, `include`)
216
+ * plus the `[workflow]` fields and array-of-table item keys/values.
217
+ * `[agent]` is prose the LLM reads, not a code-consumed schema, so its keys
218
+ * are open by design; `include` is validated by resolveIncludes.
124
219
  * @param {object} data - a single layer, as parsed (never a merged tree)
125
- * @returns {string[]} dotted paths (caller prefixes with layer info)
220
+ * @returns {Array<{path: string, kind: 'unknown'|'invalid', detail?: string}>} caller prefixes with layer info
126
221
  */
127
222
  function findUnknownKeys(data) {
128
223
  const unknown = [];
129
224
  if (!isPlainObject(data)) return unknown;
130
225
  for (const [key, value] of Object.entries(data)) {
131
226
  if (!TOP_LEVEL_KEYS.has(key)) {
132
- unknown.push(key);
227
+ unknown.push({ path: key, kind: 'unknown' });
133
228
  continue;
134
229
  }
135
230
  if (key === 'workflow' && isPlainObject(value)) unknown.push(...findUnknownWorkflowKeys(value));
@@ -140,9 +235,12 @@ function findUnknownKeys(data) {
140
235
  module.exports = {
141
236
  findUnknownKeys,
142
237
  findUnknownWorkflowKeys,
238
+ findInvalidReviewLayerValues,
239
+ isArrayOfStrings,
143
240
  WORKFLOW_KEYS,
144
241
  PARTY_MEMBER_KEYS,
145
242
  PARTY_GROUP_KEYS,
146
243
  REVIEW_LAYER_KEYS,
244
+ VALID_REVIEW_TIERS,
147
245
  TOP_LEVEL_KEYS,
148
246
  };