@unbox-plus/cli 0.20.4

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 (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+ // GATE DE COBERTURA EDITÁVEL — o que a LOJA mostra e o lojista NÃO consegue editar.
3
+ //
4
+ // Mede TODAS as páginas que a loja declara em `GET /api/unbox/paginas`, não só a home. Antes media
5
+ // uma página só, a do `--url`: uma loja com /sobre inteira fora dos primitivos passava com 95%,
6
+ // porque ninguém tinha olhado o /sobre. Gate que não olha aprova sem ter olhado.
7
+ //
8
+ // De cada página abre a loja em modo edição (?unbox_editor=1), espera o manifesto dos primitivos e
9
+ // mede: de todo texto visível e toda imagem, quanto está dentro de um `[data-editor-path]`. Lista o
10
+ // que ficou de fora, com trecho, DIZENDO EM QUE PÁGINA. E lê do manifesto de cada página os dois
11
+ // defeitos de construção que a cobertura sozinha não pega: seção sem `kind`/`label` (o painel
12
+ // mostraria id de código ao lojista) e editável FORA de container (primitivo bom, página que ninguém
13
+ // declarou: ele NÃO é editável, e a loja o denuncia em `manifest.semContainer`).
14
+ //
15
+ // A loja também diz quais páginas são SÓ CABEÇALHO E RODAPÉ POR REGRA (`soChrome: true` na lista; README
16
+ // §8: texto legal, rótulo de formulário/UI e mecânica de compra não viram primitivo). /termos, /privacidade,
17
+ // /devolucoes e /busca medem 22% a 65% POR DESENHO, e um gate que as reprova para sempre é um gate que
18
+ // ninguém lê. Nelas a régua de --minimo não vale: a cobertura é impressa, não julgada. O resto vale igual
19
+ // (zero editável fora de container, zero seção sem tipo/rótulo, manifesto tem de chegar). O gate NÃO infere
20
+ // isso do conteúdo: é a loja que declara, em lib/rotas-editaveis.ts (SO_CHROME), com o motivo ao lado;
21
+ // página que ela não marcou é página comum.
22
+ //
23
+ // E a loja DECLARA OS CONTAINERS DE CADA ROTA (`containers` na lista; tabela CONTAINERS_POR_ROTA em
24
+ // lib/rotas-editaveis.ts): é com isso que o editor avisa "isto também aparece em «Categoria»" antes de alguém
25
+ // abrir /categoria, e que a publicação alcança a página certa na primeira sessão. Uma tabela digitada envelhece
26
+ // em silêncio, então o gate CONFERE: os containers que o manifesto de cada página mostrou (fora o `chrome`, que é
27
+ // do site inteiro) têm de ser exatamente os declarados. Container que aparece sem estar declarado, container
28
+ // declarado que não aparece, ou rota que a loja lista sem declarar nada: REPROVADO, dizendo a rota e o container.
29
+ //
30
+ // Saída: 0 = toda página >= --minimo (default 0.8), fora as que a loja declarou só cabeçalho e rodapé
31
+ // por regra, nenhuma seção sem kind/label, nenhum editável fora de container e a declaração de containers
32
+ // batendo em toda página · 1 = REPROVADO (alguma página comum abaixo do mínimo, seção sem tipo/rótulo,
33
+ // editável fora de container, ou declaração de containers divergente; o relatório diz QUAL página) · 2 = NÃO
34
+ // RODOU (servidor fora, lista de páginas indisponível, rota declarada sem exemplo para abrir, página sem
35
+ // provider, manifesto que nunca chegou). Doutrina da casa: gate que varre o vazio e diz "limpo" aprova sem ter olhado.
36
+ //
37
+ // Uso: node scripts/check-editable.mjs --url http://localhost:3000 [--minimo 0.8] [--json saida.json]
38
+ // node scripts/check-editable.mjs --url http://localhost:3000/sobre
39
+ // (`--url` com caminho mede SÓ aquela página: é a rodada de uma página, para quem está
40
+ // consertando uma rota e não quer esperar as outras.)
41
+ // node scripts/check-editable.mjs --url http://localhost:3000 --paginas /,/sobre,/produtos
42
+ // (`--paginas` é para a loja que ainda não tem /api/unbox/paginas; é uma lista digitada à
43
+ // mão, e o relatório diz isso.)
44
+ import { createRequire } from "node:module";
45
+ import { readFileSync, writeFileSync } from "node:fs";
46
+ import path from "node:path";
47
+
48
+ const args = Object.fromEntries(process.argv.slice(2).map((a, i, arr) => (a.startsWith("--") ? [a.slice(2), arr[i + 1] ?? "1"] : [])).filter((p) => p.length));
49
+ const URL_ = args.url ?? "http://localhost:3000";
50
+ const MINIMO = Number(args.minimo ?? 0.8);
51
+ const ALVO = new URL(URL_);
52
+ const ORIGEM = ALVO.origin;
53
+
54
+ function carregarPlaywright() {
55
+ const req = createRequire(import.meta.url);
56
+ const candidatos = [process.cwd(), path.dirname(new URL(import.meta.url).pathname), process.env.UNBOX_RUNNER_SCRIPTS ?? ""].filter(Boolean);
57
+ for (const base of candidatos) {
58
+ try {
59
+ return createRequire(path.join(base, "package.json"))("playwright-core");
60
+ } catch {}
61
+ }
62
+ try { return req("playwright-core"); } catch {}
63
+ return null;
64
+ }
65
+
66
+ const pw = carregarPlaywright();
67
+ if (!pw) {
68
+ console.error("[check-editable] playwright-core não encontrado (npm i -D playwright-core). NÃO RODOU.");
69
+ process.exit(2);
70
+ }
71
+
72
+ // ── A LISTA DE PÁGINAS ─────────────────────────────────────────────────────────────────────────
73
+ // Quem sabe quais páginas a loja tem é a loja (app/api/unbox/paginas/route.ts, contrato no README
74
+ // §9). Rota dinâmica entra UMA vez, pelo exemplo concreto que ela devolve — medir 24 produtos é
75
+ // medir a mesma receita 24 vezes. Rota declarada SEM exemplo (catálogo vazio, sem credenciais, ou
76
+ // fora do ar) não tem como ser medida, e isso é NÃO RODOU: ela não conta como aprovada.
77
+ // E a loja também diz quais páginas são SÓ CABEÇALHO E RODAPÉ POR REGRA (`soChrome: true`; README §8).
78
+ const CAMINHO = /^\/(?![/\\])[^\\\s]*$/;
79
+ /** a entrada como a loja a declara; `soChrome` só conta quando é o `true` literal (é a loja que decide, e só ela);
80
+ * `containers` só existe quando a loja mandou uma lista (ausente = a loja não declarou, e isso reprova) */
81
+ const declaradaPelaLoja = (p) => ({
82
+ rota: p.rota,
83
+ caminho: p.caminho,
84
+ label: p.label,
85
+ soChrome: p.soChrome === true,
86
+ ...(Array.isArray(p.containers) ? { containers: p.containers.filter((c) => typeof c === "string") } : {}),
87
+ });
88
+
89
+ async function listaDaLoja() {
90
+ let res;
91
+ try {
92
+ res = await fetch(`${ORIGEM}/api/unbox/paginas`, { signal: AbortSignal.timeout(10_000), headers: { accept: "application/json" } });
93
+ } catch (e) {
94
+ return { erro: `não consegui perguntar à loja quais páginas ela tem (${ORIGEM}/api/unbox/paginas): ${e?.message ?? e}` };
95
+ }
96
+ if (!res.ok) {
97
+ return { erro: `${ORIGEM}/api/unbox/paginas respondeu ${res.status}. Instale a rota na loja (ela deriva as páginas de app/(loja)/ e do DISALLOW do robots) ou passe --paginas /,/sobre,…` };
98
+ }
99
+ const corpo = await res.json().catch(() => null);
100
+ const lista = Array.isArray(corpo?.paginas) ? corpo.paginas : null;
101
+ if (!lista?.length) return { erro: `${ORIGEM}/api/unbox/paginas respondeu sem lista de páginas.` };
102
+ return { lista: lista.filter((p) => typeof p?.rota === "string").map(declaradaPelaLoja) };
103
+ }
104
+
105
+ async function descobrirPaginas() {
106
+ if (args.paginas) {
107
+ const lista = String(args.paginas).split(",").map((s) => s.trim()).filter(Boolean);
108
+ if (!lista.length) return { erro: "--paginas veio vazio." };
109
+ const ruins = lista.filter((p) => !CAMINHO.test(p));
110
+ if (ruins.length) return { erro: `--paginas só aceita caminhos absolutos desta loja (começando com "/"): ${ruins.join(", ")}` };
111
+ // lista digitada: a loja não foi ouvida, então nenhuma página conta como só cabeçalho e rodapé por regra,
112
+ // e a declaração de containers não tem como ser conferida
113
+ return { paginas: lista.map((p) => ({ rota: p, caminho: p, label: p, soChrome: false, semDeclaracaoPorque: "a loja não foi consultada (--paginas)" })), fonte: "--paginas (lista digitada à mão; a loja não foi consultada, e nenhuma página conta como só cabeçalho e rodapé por regra)", semExemplo: [] };
114
+ }
115
+ if (ALVO.pathname !== "/") {
116
+ const p = ALVO.pathname;
117
+ // A rodada de uma página só AINDA pergunta à loja se essa página é só cabeçalho e rodapé por regra: sem
118
+ // isso, /termos sozinha reprovaria aqui e passaria na rodada completa, e o gate diria duas coisas. Se a
119
+ // loja não responder a lista, a página é medida como comum, e o relatório diz que a loja não foi ouvida.
120
+ const declarada = await listaDaLoja();
121
+ const marcada = declarada.lista?.find((x) => x.caminho === p || x.rota === p);
122
+ const fonte = declarada.lista
123
+ ? "o caminho do --url (uma página só; sem caminho, mede todas as que a loja declara)"
124
+ : "o caminho do --url (uma página só; a loja não respondeu a lista, então esta página conta como comum)";
125
+ // a declaração de containers só é conferível quando a loja respondeu E lista esta página
126
+ const semDeclaracaoPorque = !declarada.lista ? "a loja não respondeu a lista" : !marcada ? "a loja não lista esta página em /api/unbox/paginas" : undefined;
127
+ return {
128
+ paginas: [{ rota: marcada?.rota ?? p, caminho: p, label: marcada?.label ?? p, soChrome: Boolean(marcada?.soChrome), ...(marcada?.containers ? { containers: marcada.containers } : {}), ...(semDeclaracaoPorque ? { semDeclaracaoPorque } : {}) }],
129
+ fonte,
130
+ semExemplo: [],
131
+ };
132
+ }
133
+ const declarada = await listaDaLoja();
134
+ if (declarada.erro) return declarada;
135
+ const comExemplo = declarada.lista.filter((p) => typeof p.caminho === "string" && CAMINHO.test(p.caminho));
136
+ const semExemplo = declarada.lista.filter((p) => !(typeof p.caminho === "string" && CAMINHO.test(p.caminho))).map((p) => p.rota);
137
+ if (!comExemplo.length) return { erro: `${ORIGEM}/api/unbox/paginas não trouxe nenhuma página com caminho para abrir.` };
138
+ return { paginas: comExemplo, fonte: "a própria loja (/api/unbox/paginas)", semExemplo };
139
+ }
140
+
141
+ const achado = await descobrirPaginas();
142
+ if (achado.erro) {
143
+ console.error(`[check-editable] ${achado.erro} NÃO RODOU.`);
144
+ process.exit(2);
145
+ }
146
+
147
+ // ── A MEDIÇÃO, PÁGINA A PÁGINA ─────────────────────────────────────────────────────────────────
148
+ const browser = await pw.chromium.launch({ channel: "chrome", headless: true }).catch(async () => pw.chromium.launch({ headless: true }));
149
+
150
+ async function medir(caminho) {
151
+ const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
152
+ let manifest = null;
153
+ await page.exposeFunction("__unboxManifest", (m) => { manifest = m; });
154
+ await page.addInitScript(() => {
155
+ // o provider fala com window.parent; aqui não há pai, então interceptamos o postMessage do próprio window
156
+ const orig = window.postMessage.bind(window);
157
+ Object.defineProperty(window, "parent", { get: () => ({ postMessage: (m) => { if (m && m.type === "unbox-editor:manifest") window.__unboxManifest(m.manifest); } }) });
158
+ void orig;
159
+ });
160
+ const u = new URL(caminho, ORIGEM);
161
+ u.searchParams.set("unbox_editor", "1");
162
+ const res = await page.goto(u.toString(), { waitUntil: "load", timeout: 60_000 }).catch((e) => ({ erro: e }));
163
+ if (!res || res.erro || (res.status && res.status() >= 400)) {
164
+ await page.close();
165
+ return { naoRodou: `não abriu ${u}: ${res?.erro?.message ?? res?.status?.()}` };
166
+ }
167
+ // porta /acesso no caminho? então o gate não viu a loja
168
+ if (page.url().includes("/acesso")) {
169
+ await page.close();
170
+ return { naoRodou: `${caminho} caiu na porta /acesso; rode com PREVIEW_DISABLED=1 ou passe o token` };
171
+ }
172
+ for (let i = 0; i < 40 && !manifest; i++) await page.waitForTimeout(250);
173
+ if (!manifest) {
174
+ await page.close();
175
+ return { naoRodou: `${caminho} nunca publicou manifesto (EditableProvider ausente nesta rota, sem ?unbox_editor, ou NEXT_PUBLIC_EDITOR_ORIGIN vazio no env da loja; o modo edição exige a origem do editor)` };
176
+ }
177
+ const medido = await page.evaluate(() => {
178
+ const ignorar = (el) => {
179
+ if (!el) return true;
180
+ const cs = getComputedStyle(el);
181
+ if (cs.display === "none" || cs.visibility === "hidden") return true;
182
+ if (el.closest("script,style,noscript,template,[aria-hidden='true'],.sr-only,[data-editor-ignore],[data-nosnippet],dialog,[role='dialog'],nav [role='menu']")) return true;
183
+ return false;
184
+ };
185
+ const dentro = (el) => Boolean(el.closest("[data-editor-path]"));
186
+ const textos = [];
187
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
188
+ let n;
189
+ while ((n = walker.nextNode())) {
190
+ const t = n.textContent.replace(/\s+/g, " ").trim();
191
+ if (t.length < 3) continue;
192
+ const el = n.parentElement;
193
+ if (ignorar(el)) continue;
194
+ // preço, número puro e rótulo de formulário não são copy do lojista
195
+ if (/^(R\$\s?)?[\d.,%\s]+$/.test(t)) continue;
196
+ if (el.closest("input,select,textarea,button[type='submit'],form label")) continue;
197
+ textos.push({ texto: t.slice(0, 80), tag: el.tagName.toLowerCase(), ok: dentro(el), secao: el.closest("[data-editor-section]")?.dataset.editorSection ?? null });
198
+ }
199
+ const imagens = [...document.querySelectorAll("img,video,picture source")].filter((el) => !ignorar(el)).map((el) => ({
200
+ src: (el.currentSrc || el.getAttribute("src") || el.getAttribute("srcset") || "").slice(0, 100),
201
+ ok: dentro(el),
202
+ secao: el.closest("[data-editor-section]")?.dataset.editorSection ?? null,
203
+ decorativa: el.getAttribute("alt") === "" || el.getAttribute("aria-hidden") === "true",
204
+ })).filter((i) => !i.decorativa && i.src && !/\.svg(\?|$)/.test(i.src));
205
+ return { textos, imagens };
206
+ });
207
+ await page.close();
208
+ // texto repetido (marquee com 10 cópias, faixa de anúncio) conta UMA vez: o que se
209
+ // mede é o que o lojista precisaria editar, não quantas vezes a tela repete.
210
+ const unicos = (lista, chave) => {
211
+ const vistos = new Map();
212
+ for (const item of lista) {
213
+ const k = chave(item);
214
+ const prev = vistos.get(k);
215
+ if (!prev || (!prev.ok && item.ok)) vistos.set(k, item);
216
+ }
217
+ return [...vistos.values()];
218
+ };
219
+ const textos = unicos(medido.textos, (t) => `${t.secao}|${t.texto}`);
220
+ const imagens = unicos(medido.imagens, (i) => `${i.secao}|${i.src}`);
221
+ const total = textos.length + imagens.length;
222
+ if (total === 0) return { naoRodou: `${caminho} não tem texto nem imagem visível: nada medido` };
223
+ const cobertos = textos.filter((t) => t.ok).length + imagens.filter((i) => i.ok).length;
224
+ const fora = [
225
+ ...textos.filter((t) => !t.ok).map((t) => `texto ${t.secao ?? "(fora de seção)"} <${t.tag}> "${t.texto}"`),
226
+ ...imagens.filter((i) => !i.ok).map((i) => `imagem ${i.secao ?? "(fora de seção)"} ${i.src}`),
227
+ ];
228
+ return { url: u.toString(), manifest, total, cobertos, cobertura: cobertos / total, fora };
229
+ }
230
+
231
+ /**
232
+ * Uma segunda chance quando a página não abriu no prazo: em `next dev` a primeira visita compila a
233
+ * rota, e oito páginas em sequência estouraram os 60 s justamente na home e em /busca numa rodada em
234
+ * que as outras seis abriram (medido numa loja piloto). Reprovar por isso seria reprovar o compilador.
235
+ * Só o prazo de abertura ganha a segunda chance; manifesto que não chegou e página vazia são NÃO RODOU
236
+ * de primeira. E qualquer estouro inesperado do Playwright vira NÃO RODOU daquela página, com o
237
+ * motivo, em vez de derrubar o gate inteiro com exit 1, que seria lido como reprovação.
238
+ */
239
+ async function medirComSegundaChance(caminho) {
240
+ let r;
241
+ for (let tentativa = 1; tentativa <= 2; tentativa++) {
242
+ try {
243
+ r = await medir(caminho);
244
+ } catch (e) {
245
+ r = { naoRodou: `${caminho}: ${String(e?.message ?? e).split("\n")[0]}` };
246
+ }
247
+ if (!r.naoRodou || !/Timeout \d+ms exceeded/.test(r.naoRodou)) return r;
248
+ if (tentativa === 1) console.log(` (${caminho} não abriu no prazo; segunda tentativa, porque em next dev a primeira visita compila a rota)`);
249
+ }
250
+ return r;
251
+ }
252
+
253
+ const resultados = [];
254
+ for (const p of achado.paginas) {
255
+ const r = await medirComSegundaChance(p.caminho);
256
+ resultados.push({ ...p, ...r });
257
+ }
258
+ await browser.close();
259
+
260
+ // ── O RELATÓRIO ────────────────────────────────────────────────────────────────────────────────
261
+ console.log(`[check-editable] ${ORIGEM} · ${achado.paginas.length} página(s), lista veio de: ${achado.fonte}`);
262
+ const semExemplo = achado.semExemplo ?? [];
263
+ if (semExemplo.length) {
264
+ console.log(`\nROTAS DECLARADAS SEM EXEMPLO PARA ABRIR (${semExemplo.length}), NÃO foram medidas: ${semExemplo.join(", ")}`);
265
+ console.log(" (a loja não devolveu um caminho concreto para elas: catálogo vazio, sem credenciais da Unbox neste ambiente, ou fora do ar)");
266
+ }
267
+ const porRegra = achado.paginas.filter((p) => p.soChrome);
268
+ if (porRegra.length) {
269
+ console.log(`\nSÓ CABEÇALHO E RODAPÉ, POR REGRA (§8), declaradas pela loja (${porRegra.length}): ${porRegra.map((p) => p.caminho).join(", ")}`);
270
+ console.log(" (texto legal, rótulo de formulário e de UI e mecânica de compra não viram primitivo; nelas a cobertura é impressa, não julgada, e o resto do gate vale)");
271
+ }
272
+
273
+ // TIPO E RÓTULO DE SEÇÃO (07/09): toda seção nasce com `kind` (vocabulário fechado) e `label`.
274
+ // Sem isso o painel mostra id de código para o lojista — reprova. A conta é da UNIÃO das páginas:
275
+ // a mesma seção declarada em duas rotas é uma só, e o relatório diz onde ela foi vista primeiro.
276
+ // O VOCABULÁRIO vem do document.ts da própria loja (lib/editable/document.ts, que a foundation copia para
277
+ // cá): um `kind` novo entra lá e o gate passa a aceitá-lo sem ninguém lembrar de uma segunda lista, que
278
+ // era o que havia (e reprovava seção legítima assim que o vocabulário crescia). A lista interna é só a
279
+ // reserva para quando o arquivo não está onde a foundation o põe, e o relatório diz de onde veio.
280
+ const KINDS_RESERVA = ["cabecalho", "faixa-de-anuncio", "banner", "texto-rolante", "vitrine-de-produtos", "produto-em-destaque", "beneficios", "como-funciona", "depoimentos", "perguntas-frequentes", "galeria", "video", "sobre-a-marca", "comparacao", "newsletter", "contato", "lojas-fisicas", "botao-flutuante", "rodape", "outro"];
281
+ function vocabularioDeKinds() {
282
+ const candidatos = [new URL("../lib/editable/document.ts", import.meta.url), path.join(process.cwd(), "lib", "editable", "document.ts")];
283
+ for (const c of candidatos) {
284
+ try {
285
+ const m = readFileSync(c, "utf8").match(/export const SECTION_KINDS\s*=\s*\[([\s\S]*?)\]\s*as const/);
286
+ const kinds = m ? [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]) : [];
287
+ if (kinds.length) return { kinds, fonte: "lib/editable/document.ts" };
288
+ } catch {}
289
+ }
290
+ return { kinds: KINDS_RESERVA, fonte: "lista interna do gate (lib/editable/document.ts não encontrado)" };
291
+ }
292
+ const { kinds: SECTION_KINDS, fonte: FONTE_DOS_KINDS } = vocabularioDeKinds();
293
+ const semTipo = new Map();
294
+ for (const r of resultados) {
295
+ for (const s of r.manifest?.sections ?? []) {
296
+ if (s.item || (s.kind && SECTION_KINDS.includes(s.kind) && s.label)) continue;
297
+ if (!semTipo.has(`${s.container}/${s.id}`)) semTipo.set(`${s.container}/${s.id}`, { ...s, onde: r.caminho });
298
+ }
299
+ }
300
+
301
+ // EDITÁVEL FORA DE CONTAINER (foundation 11): a seção foi escrita com primitivos, mas está sendo
302
+ // renderizada solta, sem `Editable.Sections container=…`/`Editable.Section` em volta. O caminho dela
303
+ // cairia na RAIZ do documento, onde o mesmo nome vale para o site inteiro: o `titulo` do FAQ e o
304
+ // `titulo` da newsletter seriam o MESMO valor, em todas as páginas. A loja recusa (deixa o literal do
305
+ // código na tela) e declara em `manifest.semContainer`. Não é cobertura baixa por falta de primitivo:
306
+ // é primitivo bom em página que ninguém declarou. Reprova sozinho, e diz em que página está.
307
+ // (Manifesto abaixo da foundation 11 não traz o campo: aí a lista fica vazia, e a cobertura por
308
+ // página continua sendo o que pega a seção solta, porque um primitivo fora de container não recebe
309
+ // `data-editor-path`.)
310
+ const foraDeContainer = new Map();
311
+ for (const r of resultados) {
312
+ for (const x of r.manifest?.semContainer ?? []) {
313
+ const onde = x.pagina ?? r.caminho;
314
+ const atual = foraDeContainer.get(x.path);
315
+ if (!atual) foraDeContainer.set(x.path, { ...x, paginas: [onde] });
316
+ else if (!atual.paginas.includes(onde)) atual.paginas.push(onde);
317
+ }
318
+ }
319
+
320
+ // A DECLARAÇÃO DE CONTAINERS (achado 2 do Astra): a loja diz, por rota, quais containers ela renderiza
321
+ // (CONTAINERS_POR_ROTA em lib/rotas-editaveis.ts), e o editor e a publicação confiam nisso antes de a prévia
322
+ // ter aberto a página. O gate confere a declaração contra o que o manifesto mostrou: os containers de primeiro
323
+ // nível das seções e dos caminhos ("home.faq" é o `home`), fora o `chrome`, que é do site inteiro e não se lista.
324
+ // Diverge = REPROVADO, dizendo a rota e o container: é o que impede a tabela de envelhecer em silêncio.
325
+ const CONTAINER_DO_SITE = "chrome";
326
+ const raizDoContainer = (c) => String(c).split(".")[0];
327
+ function containersObservados(manifest) {
328
+ const vistos = new Set();
329
+ for (const s of manifest?.sections ?? []) if (s.container) vistos.add(raizDoContainer(s.container));
330
+ for (const e of manifest?.entries ?? []) if (e.container) vistos.add(raizDoContainer(e.container));
331
+ vistos.delete(CONTAINER_DO_SITE);
332
+ return [...vistos].sort();
333
+ }
334
+ /** por página medida: { declarados, observados, faltouDeclarar, declaradoSemAparecer } ou { naoConferido: motivo } */
335
+ function conferirDeclaracao(r) {
336
+ if (r.semDeclaracaoPorque) return { naoConferido: r.semDeclaracaoPorque };
337
+ const observados = containersObservados(r.manifest);
338
+ if (!r.containers) return { observados, semDeclaracao: true };
339
+ const declarados = [...new Set(r.containers.map(raizDoContainer).filter((c) => c !== CONTAINER_DO_SITE))].sort();
340
+ return {
341
+ declarados,
342
+ observados,
343
+ faltouDeclarar: observados.filter((c) => !declarados.includes(c)),
344
+ declaradoSemAparecer: declarados.filter((c) => !observados.includes(c)),
345
+ };
346
+ }
347
+ for (const r of resultados) if (!r.naoRodou) r.declaracao = conferirDeclaracao(r);
348
+ const semDeclaracao = resultados.filter((r) => r.declaracao?.semDeclaracao);
349
+ const divergentes = resultados.filter((r) => r.declaracao && !r.declaracao.naoConferido && !r.declaracao.semDeclaracao && (r.declaracao.faltouDeclarar.length || r.declaracao.declaradoSemAparecer.length));
350
+
351
+ const naoRodaram = resultados.filter((r) => r.naoRodou);
352
+ const medidas = resultados.filter((r) => !r.naoRodou);
353
+ // a página só cabeçalho e rodapé por regra não entra na régua: a cobertura dela é impressa, não julgada
354
+ const medidasPorRegra = medidas.filter((r) => r.soChrome);
355
+ const descobertas = medidas.filter((r) => !r.soChrome && r.cobertura < MINIMO);
356
+
357
+ console.log("\nCOBERTURA POR PÁGINA:");
358
+ for (const r of resultados) {
359
+ if (r.naoRodou) {
360
+ console.log(` NÃO RODOU ${r.caminho.padEnd(28)} ${r.naoRodou}`);
361
+ continue;
362
+ }
363
+ const marca = r.soChrome ? "por regra " : r.cobertura >= MINIMO ? "ok " : "DESCOBERTA";
364
+ const nota = r.soChrome ? " · só cabeçalho e rodapé, por regra (§8)" : "";
365
+ const soltos = (r.manifest.semContainer ?? []).length;
366
+ console.log(` ${marca} ${r.caminho.padEnd(28)} ${(r.cobertura * 100).toFixed(1)}% (${r.cobertos}/${r.total})${nota} · manifesto: ${r.manifest.entries.length} caminhos · ${r.manifest.sections.length} seções${soltos ? ` · ${soltos} editável(is) FORA de container` : ""}`);
367
+ }
368
+
369
+ for (const r of descobertas) {
370
+ console.log(`\nFORA DOS PRIMITIVOS em ${r.caminho} (${r.fora.length}):`);
371
+ for (const f of r.fora.slice(0, 40)) console.log(" " + f);
372
+ if (r.fora.length > 40) console.log(` … +${r.fora.length - 40}`);
373
+ }
374
+
375
+ if (foraDeContainer.size) {
376
+ console.log(`\nEDITÁVEIS FORA DE CONTAINER (${foraDeContainer.size}): a página os renderiza soltos, então eles NÃO são editáveis:`);
377
+ for (const [p, x] of [...foraDeContainer].slice(0, 40)) console.log(` ${p} · ${x.type}${x.label ? ` · ${x.label}` : ""} · em ${x.paginas.join(", ")}`);
378
+ if (foraDeContainer.size > 40) console.log(` … +${foraDeContainer.size - 40}`);
379
+ console.log(` Envolva as seções dessa(s) página(s) em <Editable.Sections container="<nome-da-página>"> com um <Editable.Section id=…> por seção.`);
380
+ console.log(` A ordem é do container e a copy é do container: duas páginas no MESMO container dividem as duas de propósito; página que quer copy própria declara container próprio.`);
381
+ }
382
+
383
+ if (semTipo.size) {
384
+ console.log(`\nSEÇÕES SEM TIPO OU RÓTULO (${semTipo.size}): toda Editable.Section precisa de kind (um de: ${SECTION_KINDS.join(", ")}; vocabulário lido de ${FONTE_DOS_KINDS}) e label:`);
385
+ for (const [chave, s] of semTipo) console.log(` ${chave} · kind=${s.kind ?? "(vazio)"} · label=${s.label ?? "(vazio)"} · visto em ${s.onde}`);
386
+ }
387
+
388
+ if (medidas.length) {
389
+ const lista = (xs) => (xs.length ? xs.join(", ") : "(nenhum)");
390
+ console.log("\nCONTAINERS POR PÁGINA (o que a loja declara em CONTAINERS_POR_ROTA × o que o manifesto mostrou, fora o chrome):");
391
+ for (const r of medidas) {
392
+ const d = r.declaracao;
393
+ if (d.naoConferido) {
394
+ console.log(` não conferido ${r.caminho.padEnd(24)} observados: ${lista(d.observados ?? containersObservados(r.manifest))} · ${d.naoConferido}`);
395
+ continue;
396
+ }
397
+ if (d.semDeclaracao) {
398
+ console.log(` SEM DECLARAÇÃO ${r.caminho.padEnd(23)} observados: ${lista(d.observados)} · a loja lista a rota ${r.rota} sem \`containers\`: acrescente-a a CONTAINERS_POR_ROTA em lib/rotas-editaveis.ts`);
399
+ continue;
400
+ }
401
+ const diverge = d.faltouDeclarar.length || d.declaradoSemAparecer.length;
402
+ const detalhe = [d.faltouDeclarar.length ? `faltou declarar: ${d.faltouDeclarar.join(", ")}` : "", d.declaradoSemAparecer.length ? `declarado sem aparecer: ${d.declaradoSemAparecer.join(", ")}` : ""].filter(Boolean).join(" · ");
403
+ console.log(` ${diverge ? "DIVERGE " : "ok "} ${r.caminho.padEnd(24)} declarados: ${lista(d.declarados)} · observados: ${lista(d.observados)}${detalhe ? ` · ${detalhe}` : ""}`);
404
+ }
405
+ if (divergentes.length || semDeclaracao.length) console.log(" A tabela é a promessa que o editor e a publicação usam antes de abrir a página: quem mexeu na página atualiza a linha dela em lib/rotas-editaveis.ts (rota → containers que ela renderiza, fora o chrome).");
406
+ }
407
+
408
+ if (args.json) {
409
+ writeFileSync(args.json, JSON.stringify({
410
+ origem: ORIGEM,
411
+ fonteDaLista: achado.fonte,
412
+ minimo: MINIMO,
413
+ rotasSemExemplo: semExemplo,
414
+ paginas: resultados.map((r) => ({
415
+ rota: r.rota,
416
+ caminho: r.caminho,
417
+ cobertura: r.cobertura ?? null,
418
+ total: r.total ?? 0,
419
+ cobertos: r.cobertos ?? 0,
420
+ fora: r.fora ?? [],
421
+ foraDeContainer: r.manifest?.semContainer ?? [],
422
+ naoRodou: r.naoRodou ?? null,
423
+ ...(r.soChrome ? { soChrome: true } : {}),
424
+ ...(r.declaracao ? { containers: r.declaracao } : {}),
425
+ })),
426
+ secoesSemTipo: [...semTipo.keys()],
427
+ editaveisForaDeContainer: [...foraDeContainer].map(([p, x]) => ({ path: p, type: x.type, label: x.label ?? null, paginas: x.paginas })),
428
+ }, null, 2));
429
+ }
430
+
431
+ // O VEREDITO. O que não foi medido vem primeiro, mas sem esconder o que já reprovou: quem lê o
432
+ // relatório conserta as duas coisas numa rodada só, em vez de descobrir a segunda depois de
433
+ // consertar a primeira.
434
+ const reprovacoes = [];
435
+ if (descobertas.length) reprovacoes.push(`página(s) descoberta(s): ${descobertas.map((r) => `${r.caminho} (${(r.cobertura * 100).toFixed(1)}%)`).join(", ")}`);
436
+ if (foraDeContainer.size) reprovacoes.push(`${foraDeContainer.size} editável(is) fora de container em ${[...new Set([...foraDeContainer.values()].flatMap((x) => x.paginas))].join(", ")}`);
437
+ if (semTipo.size) reprovacoes.push(`${semTipo.size} seção(ões) sem tipo ou rótulo`);
438
+ if (divergentes.length) {
439
+ reprovacoes.push(`declaração de containers divergente em ${divergentes.map((r) => {
440
+ const d = r.declaracao;
441
+ const partes = [d.faltouDeclarar.length ? `faltou declarar ${d.faltouDeclarar.join(", ")}` : "", d.declaradoSemAparecer.length ? `declarado sem aparecer ${d.declaradoSemAparecer.join(", ")}` : ""].filter(Boolean);
442
+ return `${r.rota} (${partes.join("; ")})`;
443
+ }).join(", ")}`);
444
+ }
445
+ if (semDeclaracao.length) reprovacoes.push(`rota(s) listada(s) pela loja sem declaração de containers: ${semDeclaracao.map((r) => r.rota).join(", ")}`);
446
+
447
+ if (naoRodaram.length || semExemplo.length) {
448
+ const partes = [];
449
+ if (naoRodaram.length) partes.push(`${naoRodaram.length} de ${resultados.length} página(s) NÃO foram medidas: ${naoRodaram.map((r) => r.caminho).join(", ")}`);
450
+ if (semExemplo.length) partes.push(`${semExemplo.length} rota(s) declarada(s) sem exemplo para abrir: ${semExemplo.join(", ")}`);
451
+ if (reprovacoes.length) partes.push(`e, do que foi medido, já reprovou: ${reprovacoes.join(" · ")}`);
452
+ console.error(`\n[check-editable] ${partes.join(" · ")}. NÃO RODOU.`);
453
+ process.exit(2);
454
+ }
455
+ if (reprovacoes.length) {
456
+ console.error(`\n[check-editable] REPROVADO: ${reprovacoes.join(" · ")}`);
457
+ process.exit(1);
458
+ }
459
+ // "1 página(s), 1 só cabeçalho e rodapé por regra": a parte comum só aparece quando houve página comum medida
460
+ const comuns = medidas.length - medidasPorRegra.length;
461
+ const resumo = medidasPorRegra.length
462
+ ? `${comuns ? `${comuns} >= ${(MINIMO * 100).toFixed(0)}% e ` : ""}${medidasPorRegra.length} só cabeçalho e rodapé por regra (§8)`
463
+ : `todas >= ${(MINIMO * 100).toFixed(0)}%`;
464
+ const conferidas = medidas.filter((r) => r.declaracao && !r.declaracao.naoConferido).length;
465
+ const declaracao = conferidas ? `, declaração de containers batendo em ${conferidas} página(s)` : ", declaração de containers não conferida (a loja não foi ouvida)";
466
+ console.log(`\n[check-editable] aprovado: ${medidas.length} página(s), ${resumo}, nenhuma seção sem tipo, nenhum editável fora de container${declaracao}`);
467
+ process.exit(0);
@@ -0,0 +1,111 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // GATE DE HONESTIDADE (M5) — o bloqueio é contra fabricação
3
+ // SILENCIOSA, não contra a escolha do lojista. Roda no QA visual (agente 16) e
4
+ // sempre que quiser: npm run unbox:honestidade
5
+ //
6
+ // A foundation NÃO nasce mais com placeholder de prova social ("793 vendidos",
7
+ // "25.347 avaliações", Cliente A/B/C, estoque que desce sozinho): tudo isso foi ao
8
+ // ar em loja real e saiu na v0.15.6. Regra: dado que a loja não tem, o bloco não
9
+ // renderiza. Num scaffold novo este gate sai VERDE. Ele existe para pegar o que o
10
+ // briefing ou alguém adicionar depois sem lastro — e o lojista continua podendo
11
+ // decidir manter algo (a loja é dele), registrando na allowlist abaixo.
12
+ //
13
+ // Decisão tomada (dado real confirmado OU o lojista mandou manter)? Registre a
14
+ // substring exata em marca/honestidade-permitido.txt (uma por linha, com quem
15
+ // decidiu anotado no DESIGN-<MARCA>.md) — a linha para de ser flagrada.
16
+ //
17
+ // ── O QUE ESTE GATE VÊ, E O QUE ELE NÃO VÊ ──────────────────────────────────
18
+ // Ele lê CÓDIGO-FONTE (.ts/.tsx em components, app e lib), não a página renderizada.
19
+ // Isso é de propósito: componente que não renderiza no ambiente atual — o grid do
20
+ // catálogo com a loja em modo mockup, por exemplo — continua sendo varrido. A
21
+ // fabricação que aparece só no dia em que o catálogo real é conectado, depois da
22
+ // revisão humana, é justamente a mais perigosa.
23
+ //
24
+ // O limite real dele é OUTRO: ele só acha o que algum padrão descreve. Toda
25
+ // fabricação que passou até hoje passou por falta de padrão, não por falta de
26
+ // alcance. Achou uma? Some com ela E adicione o padrão genérico aqui — senão ela
27
+ // volta no próximo componente.
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ import fs from "node:fs";
30
+ import path from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+
33
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
34
+
35
+ // Padrões de fabricação conhecidos da foundation + genéricos suspeitos.
36
+ // [regex, explicação curta do porquê é mentira até prova em contrário]
37
+ const PATTERNS = [
38
+ [/vendidos esta semana/i, "contador de vendas fixo, sem dado real"],
39
+ [/\b25\.?347\b/, "número de avaliações fabricado da foundation"],
40
+ [/Cliente [A-C]\./, "depoimento placeholder (Cliente A/B/C)"],
41
+ [/Quem usa, recomenda/i, "bloco de depoimentos placeholder"],
42
+ [/ratingValue["':\s]*["'`]?\d|reviewCount["':\s]*(String\()?["'`]?\d/, "nota/contagem LITERAL em dado estruturado (Google): só com avaliações reais"],
43
+ [/MAIS VENDIDO/, "selo de venda sem dado de venda"],
44
+ [/devolu[cç][aã]o em (at[eé] )?30 dias|devolvemos seu dinheiro em at[eé] 30 dias/i, "política de devolução: confirme se existe MESMO"],
45
+ [/validade (?:é )?de 24 meses/i, "validade inventada: leia do rótulo real"],
46
+ [/Apenas .*unidades.*neste pre[cç]o/i, "urgência de estoque fabricada"],
47
+ [/ratingCount\s*=\s*"[\d.,]+k?"/, "contagem de avaliações default no código"],
48
+ // ── Promessas e contadores que apareciam nos docs dos módulos (CRO/SEO) sem dado por trás ──
49
+ [/\d+\s*pessoas\s+(?:compraram|est[ãa]o vendo|visualizando|acabaram de)/i, "contador de compras/visitas fabricado"],
50
+ [/compraram\s+(?:hoje|nas [úu]ltimas\s*\d+\s*(?:h|horas))/i, "contador de compras por janela de tempo sem dado real"],
51
+ [/frete gr[áa]tis (?:acima de|a partir de) R\$\s*\d/i, "frete grátis LITERAL: o valor vem de FREE_SHIPPING_THRESHOLD, e só se existir"],
52
+ [/entrega r[áa]pida/i, "promessa de prazo sem SLA real (é do lojista, não da foundation)"],
53
+ [/satisfa[cç][aã]o garantida/i, "garantia sem política real por trás"],
54
+ [/troc(?:a|amos|as)(?: gr[áa]tis)? em (?:at[eé] )?(?!7 dias)\d+ dias/i, "prazo de troca diferente do CDC (7 dias): confirme com o lojista"],
55
+
56
+ // ── Nota/contagem LITERAL na marcação (lacuna que deixou passar o grid da PDP) ──
57
+ // Os padrões acima nomeavam números específicos da foundation ("25.347") ou props
58
+ // conhecidas. Uma nota escrita direto no JSX — <b>4,8</b> ao lado de um ícone de
59
+ // estrela, com "(2,1k)" do lado — não casava com nenhum, e sobreviveu no
60
+ // components/product/pdp/catalog-grid.tsx. Estes são genéricos de propósito: qualquer
61
+ // nota ou volume de avaliação escrito à mão precisa de decisão, não só os da foundation.
62
+ [/<b>\s*[1-5][.,]\d\s*<\/b>/, "nota LITERAL escrita na marcação: sem avaliação real, a loja não exibe nota"],
63
+ [/>\s*\(\s*\d+[.,]?\d*\s*k\s*\)\s*</i, "volume de avaliações abreviado e fixo (ex.: \"(2,1k)\") escrito à mão"],
64
+ // As duas ordens: "avaliações: 12,4 mil" e "12,4 mil avaliações".
65
+ [/(?:avalia[cç][oõ]es|reviews)["'`:\s>]+\d[\d.,]*\s*(?:k|mil)?\b/i, "contagem de avaliações fixa no texto"],
66
+ [/\d[\d.,]*\s*(?:k|mil)\s+(?:avalia[cç][oõ]es|reviews|clientes|compradores)/i, "volume fixo de avaliações/clientes no texto"],
67
+ [/\b[1-5][.,]\d\s*(?:de\s*5|\/\s*5|estrelas)\b/i, "nota fixa em texto corrido"],
68
+ ];
69
+
70
+ const SCAN_DIRS = ["components", "app", "lib"];
71
+ const EXTS = new Set([".tsx", ".ts"]);
72
+
73
+ const allowPath = path.join(ROOT, "marca", "honestidade-permitido.txt");
74
+ const allow = fs.existsSync(allowPath)
75
+ ? fs.readFileSync(allowPath, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"))
76
+ : [];
77
+
78
+ const findings = [];
79
+ function scan(dir) {
80
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
81
+ const p = path.join(dir, entry.name);
82
+ if (entry.isDirectory()) { if (entry.name !== "node_modules") scan(p); continue; }
83
+ if (!EXTS.has(path.extname(entry.name))) continue;
84
+ const lines = fs.readFileSync(p, "utf8").split("\n");
85
+ lines.forEach((line, i) => {
86
+ // Comentário não chega ao usuário — o gate mira o que a loja EXIBE.
87
+ const t = line.trim();
88
+ if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") || t.startsWith("{/*")) return;
89
+ for (const [re, why] of PATTERNS) {
90
+ if (!re.test(line)) continue;
91
+ if (allow.some((a) => line.includes(a))) continue;
92
+ findings.push({ file: path.relative(ROOT, p), line: i + 1, text: line.trim().slice(0, 110), why });
93
+ break;
94
+ }
95
+ });
96
+ }
97
+ }
98
+ for (const d of SCAN_DIRS) { const p = path.join(ROOT, d); if (fs.existsSync(p)) scan(p); }
99
+
100
+ if (findings.length) {
101
+ console.error(`\n✗ HONESTIDADE — ${findings.length} item(ns) fabricado(s) SEM decisão do lojista:\n`);
102
+ for (const f of findings) {
103
+ console.error(` ${f.file}:${f.line} [${f.why}]`);
104
+ console.error(` ${f.text}`);
105
+ }
106
+ console.error(`\n Cada item precisa de UMA decisão: dado real, estado honesto, ou o lojista mandou`);
107
+ console.error(` manter (aí registre a substring em marca/honestidade-permitido.txt). O que não`);
108
+ console.error(` pode é ir pro ar sem ninguém ter sido perguntado.\n`);
109
+ process.exit(1);
110
+ }
111
+ console.log("✓ Honestidade ok: nada fabricado sem decisão do lojista.");