feat: harden portfolio and add SEO foundations
This commit is contained in:
1 parent
b079afdd73
commit
62bcfbd8fa
121 files changed
+7769
-10469
No files matched your search
@@ -0,0 +1,903 @@
|
||||
# Portfolio Hardening Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current checkout. Do not dispatch writing agents: one editor owns the dirty working tree. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Tornar conteúdo, acessibilidade, movimento, dependências, analytics, deploy e validação do portfólio previsíveis, com menos JavaScript e sem alterar sua identidade visual.
|
||||
|
||||
**Architecture:** Conteúdo de projetos continua estático e recebe apenas uma projeção observacional de saúde de demo; falha de monitoramento nunca remove card. O shell deixa de instalar scroll/animação global, mantendo Motion local e GSAP somente na história do notebook. Dependências exatas, lock congelado, consentimento opt-in e CI formam o limite operacional.
|
||||
|
||||
**Tech Stack:** Next.js 16.3.0 App Router, React 19.1.0, TypeScript 5.9.2, Tailwind CSS 4.1.12, Motion 12.23.12, GSAP 3.13.0, React Three Fiber 9.5.0, Jest 30.3.0, Biome 2.2.0, Node 22 e pnpm 9.15.9.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-04-portfolio-hardening-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Trabalhar no checkout atual; não criar worktree porque mudanças não commitadas são parte do estado-alvo e haverá um único editor.
|
||||
- Não executar branch, commit, reset, checkout, stash nem comando Git destrutivo.
|
||||
- Preservar `data/project-health.json` e o `globals.css` não rastreado da raiz sem qualquer edição.
|
||||
- Evoluir, sem restaurar, o conteúdo atual de `lib/portfolio-data.ts` e `lib/portfolio-data.load.test.ts`.
|
||||
- Editar somente `app/globals.css`; nunca importar, mover ou apagar o `globals.css` da raiz.
|
||||
- Manter preto/papel/verde ácido, fontes, hero command-center, globo, starfield, notebook, cartões e conteúdo factual existente.
|
||||
- Não criar dados, métricas, clientes, reviews, endereços, preços ou claims.
|
||||
- Não alterar metadata, JSON-LD, sitemap, robots ou conteúdo por SEO/GEO externo; essa integração terá plano separado.
|
||||
- Não adicionar dependência; remover apenas as dependências que ficarem sem consumidores.
|
||||
- Não usar suppressions do Biome, audit force, ignore de advisory, override transitivo, alias ou shim.
|
||||
- Não adicionar Playwright/Cypress; o smoke browser usa o Chromium do harness.
|
||||
- Antes de editar símbolo exportado, consultar referências via LSP; antes de excluir arquivo, repetir busca de importadores.
|
||||
- Cada checkpoint deve ficar verde antes do grupo seguinte.
|
||||
|
||||
## File Map
|
||||
|
||||
### Conteúdo e saúde de demo
|
||||
|
||||
- Modify: `lib/project-health.ts` — classificar a última observação sem filtrar conteúdo.
|
||||
- Modify: `lib/project-health.test.ts` — contrato de disponibilidade.
|
||||
- Modify: `lib/portfolio-data.ts` — projetar todo featured com estado de demo e limitar refresh a desenvolvimento.
|
||||
- Modify: `lib/portfolio-data.test.ts` — cards persistem em estados de saúde distintos.
|
||||
- Modify: `lib/portfolio-data.load.test.ts` — produção sem rede; desenvolvimento atualiza sem esvaziar.
|
||||
- Modify: `app/page.tsx` — usar o novo loader e remover ISR de monitoramento.
|
||||
- Modify: `app/components/Projects.tsx` — CTA disponível, desconhecido e indisponível.
|
||||
- Create: `app/components/Projects.test.tsx` — comportamento acessível do CTA.
|
||||
|
||||
### Shell, movimento e performance
|
||||
|
||||
- Modify: `app/layout.tsx` — remover wrappers globais; manter metadata/JSON-LD intactos.
|
||||
- Modify: `app/components/cinematic/CinematicOverlays.tsx` — manter somente overlays leves da home.
|
||||
- Delete: `app/components/cinematic/PageIntro.tsx`, `app/components/cinematic/PageIntro.test.tsx`.
|
||||
- Delete: `app/providers/SmoothScrollProvider.tsx`.
|
||||
- Delete: `app/providers/AnimationProvider.tsx`, `app/providers/AnimationProvider.test.tsx`.
|
||||
- Delete: `app/lib/animations/config.ts`, `app/lib/animations/variants.ts`.
|
||||
- Delete: `app/components/RouteTransition.tsx`.
|
||||
- Modify: `app/globals.css` — camada base de links, reduced motion, grain/marquee/divisor CSS e tokens consolidados.
|
||||
- Modify: `app/components/cinematic/CinematicHome.tsx` — retirar diretores/wrappers redundantes.
|
||||
- Modify: `app/components/cinematic/CinematicHome.test.tsx` — testar ordem de conteúdo, não mocks removidos.
|
||||
- Delete: `app/components/cinematic/CinematicSection.tsx`.
|
||||
- Delete: `app/components/story/ReelDirector.tsx`.
|
||||
- Delete: `app/hooks/useScrollReveal.ts`.
|
||||
- Rewrite: `app/components/cinematic/FilmGrain.tsx` — elemento decorativo sem canvas/RAF.
|
||||
- Rewrite: `app/components/cinematic/ScrollVelocityText.tsx` — marquee CSS sem hooks por frame.
|
||||
- Rewrite: `app/components/cinematic/SectionDivider.tsx` — DOM/SVG estático e CSS.
|
||||
- Delete: `app/components/cinematic/CustomCursor.tsx`.
|
||||
- Modify: `app/components/cinematic/ControlRoomGlobe.tsx` — interseção e visibilidade separadas.
|
||||
- Modify: `app/components/story/NotebookStory.tsx` — stage pausado em aba oculta.
|
||||
- Modify: `app/components/story/NotebookStory.test.tsx` — transição de visibilidade.
|
||||
- Modify: `app/components/Hero.tsx` — única variante ativa.
|
||||
- Modify: `app/components/Hero.test.tsx`, `app/components/Hero.reducedMotion.test.tsx` — somente comportamento observável.
|
||||
- Modify: `app/components/cinematic/HeroScene.tsx` — remover variante ignorada.
|
||||
|
||||
### Código morto e testes sem valor
|
||||
|
||||
- Delete: `app/components/ContactForm.tsx`, `app/components/ContactInfo.tsx`, `lib/usePortfolioData.ts`.
|
||||
- Delete: `app/components/CompanyLogos.tsx` e seus três testes.
|
||||
- Delete: `components/DotGrid.tsx`, `components/TextType.tsx`, `components/FaultyTerminal.tsx`, `components/Squares.tsx`, `components/LogoLoop.tsx`, `components/RotatingText.tsx`.
|
||||
- Delete: todos os nove arquivos em `components/magicui/` e `components.json`.
|
||||
- Delete: `app/components/cinematic/FloatingElements.tsx`, `HomeButton.tsx`, `HorizontalScroll.tsx`, `CinematicText.tsx`.
|
||||
- Delete: os três PBTs do Footer e os três PBTs de Experience.
|
||||
- Create: `app/components/Experience.test.tsx` — filtro featured e ordem.
|
||||
- Modify: `app/components/Footer.test.tsx` — remover asserção de classes internas.
|
||||
|
||||
### Dependências, segurança, CI e documentação
|
||||
|
||||
- Modify: `package.json`, `pnpm-lock.yaml`, `tailwind.config.js`, `nixpacks.toml`, `jest.setup.ts`.
|
||||
- Create: `.github/workflows/ci.yml`.
|
||||
- Modify: `app/components/GoogleAnalytics.tsx`.
|
||||
- Create: `app/components/GoogleAnalytics.test.tsx`.
|
||||
- Modify: `next.config.ts`.
|
||||
- Modify: `README.md`.
|
||||
- Move/replace: `docs/project-health-cron.md` → `docs/project-health.md`.
|
||||
|
||||
## Sequência e dependências
|
||||
|
||||
```text
|
||||
1 saúde pura
|
||||
└─ 2 loader
|
||||
└─ 3 apresentação de projetos
|
||||
|
||||
4 shell/foco
|
||||
└─ 5 home sem GSAP redundante
|
||||
└─ 6 efeitos sem loops
|
||||
└─ 7 pause de renderers
|
||||
|
||||
8 hero única
|
||||
9 código morto e testes
|
||||
└─ 10 dependências/lock/deploy
|
||||
└─ 11 analytics/headers
|
||||
└─ 12 CI/docs/verificação final
|
||||
```
|
||||
|
||||
## Tabela de conflitos de arquivos e interfaces
|
||||
|
||||
| Tarefas | Arquivo/interface compartilhada | Tipo de conflito | Regra de serialização |
|
||||
| --- | --- | --- | --- |
|
||||
| 1, 2 | `ProjectHealthSnapshot`, classificação de demo | Tipo e semântica | Concluir 1 antes de alterar loader |
|
||||
| 2, 3 | `PortfolioProjectView`, `loadFeaturedProjects()` | Producer/consumer | Fixar assinatura em 2; migrar UI em 3 |
|
||||
| 3, 5 | props `projects` de `CinematicHome`/`NotebookStory` | Tipo por interseção | Provar compatibilidade em 3 antes de simplificar home |
|
||||
| 4, 6, 11 | `app/layout.tsx`, `CinematicOverlays` | Shell e ordem de scripts | Executar 4 → 6 → 11; reler antes de cada edição |
|
||||
| 4, 6 | `app/globals.css` | Base, reduced motion e efeitos | Aplicar base/reduced em 4; efeitos/tokens em 6 |
|
||||
| 5, 6 | `CinematicHome.tsx` | Composição e componentes substituídos | Remover wrappers em 5; trocar efeitos em 6 |
|
||||
| 6, 7 | política de loops/visibilidade | Contrato de execução | Eliminar loops evitáveis antes de instrumentar os restantes |
|
||||
| 4, 8 | `Hero` sob shell sem wrappers | Hidratação e teste visual | Estabilizar shell antes de podar hero |
|
||||
| 5, 9, 10 | consumidores de GSAP/dependências | Grafo de imports | Apagar consumidores em 5/9; remover pacotes somente em 10 |
|
||||
| 9, 10 | `fast-check`, CVA, OGL, Lenis e plugins Tailwind | Último consumidor | Confirmar zero importadores em 9; podar manifesto em 10 |
|
||||
| 10, 11 | `package.json`, lock, `next.config.ts` | Next patch e build | Congelar toolchain antes de validar analytics/headers |
|
||||
| 4, 11 | `app/layout.tsx` e `GoogleAnalytics` | Montagem global | Layout limpo em 4; consentimento em 11 sem reintroduzir wrapper |
|
||||
| 10, 12 | scripts, lock e CI | Comandos contratuais | Workflow somente após comandos locais verdes |
|
||||
| 2, 12 | docs de project health | Semântica pública | Documentar apenas o comportamento já comprovado |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Trocar filtro de projetos por classificação de demo
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/project-health.ts`
|
||||
- Modify: `lib/project-health.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ProjectHealthCheck | undefined`.
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
export type DemoAvailability = "available" | "unavailable" | "unknown" | "none";
|
||||
|
||||
export function getDemoAvailability(
|
||||
demoUrl: string | null | undefined,
|
||||
check: ProjectHealthCheck | undefined,
|
||||
): DemoAvailability;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Substituir testes de filtragem por testes de classificação**
|
||||
|
||||
Criar casos explícitos no teste existente:
|
||||
|
||||
```ts
|
||||
expect(getDemoAvailability(null, undefined)).toBe("none");
|
||||
expect(getDemoAvailability("https://demo.example", undefined)).toBe("unknown");
|
||||
expect(getDemoAvailability("https://demo.example", snapshot.projects.online)).toBe("available");
|
||||
expect(getDemoAvailability("https://demo.example", snapshot.projects.redirect)).toBe("unavailable");
|
||||
expect(getDemoAvailability("https://demo.example", snapshot.projects.stale)).toBe("available");
|
||||
```
|
||||
|
||||
A última expectativa registra que expiração governa refresh em desenvolvimento, não a existência/estado da última observação em produção.
|
||||
|
||||
- [ ] **Step 2: Executar o teste vermelho**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec jest lib/project-health.test.ts --runInBand
|
||||
```
|
||||
|
||||
Expected: falha porque `getDemoAvailability` ainda não existe ou o filtro antigo retorna lista vazia.
|
||||
|
||||
- [ ] **Step 3: Implementar a função pura e remover `filterLiveProjects`/`isCurrentLiveCheck`**
|
||||
|
||||
Implementação mínima:
|
||||
|
||||
```ts
|
||||
export function getDemoAvailability(
|
||||
demoUrl: string | null | undefined,
|
||||
check: ProjectHealthCheck | undefined,
|
||||
): DemoAvailability {
|
||||
if (!demoUrl) return "none";
|
||||
if (!check) return "unknown";
|
||||
return check.status === 200 &&
|
||||
check.contentType?.toLowerCase().startsWith("text/html")
|
||||
? "available"
|
||||
: "unavailable";
|
||||
}
|
||||
```
|
||||
|
||||
Manter `PROJECT_HEALTH_MAX_AGE_MS` e `createProjectHealthSnapshot`; expor um helper de frescor somente para o loader de desenvolvimento.
|
||||
|
||||
- [ ] **Step 4: Confirmar o contrato verde**
|
||||
|
||||
Run o mesmo Jest. Expected: `PASS`; nenhuma chamada de rede neste teste.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Fazer o loader sempre retornar projetos editoriais
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/portfolio-data.ts`
|
||||
- Modify: `lib/portfolio-data.test.ts`
|
||||
- Modify: `lib/portfolio-data.load.test.ts`
|
||||
- Modify: `app/page.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `getDemoAvailability`, `ProjectHealthSnapshot`, `PROJECT_HEALTH_MAX_AGE_MS`.
|
||||
- Produces:
|
||||
|
||||
```ts
|
||||
export type PortfolioProjectView = PortfolioProject & {
|
||||
demoAvailability: DemoAvailability;
|
||||
demoCheckedAt: string | null;
|
||||
};
|
||||
|
||||
export function getFeaturedProjects(
|
||||
snapshot: ProjectHealthSnapshot | null,
|
||||
): PortfolioProjectView[];
|
||||
|
||||
export async function loadFeaturedProjects(): Promise<PortfolioProjectView[]>;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Atualizar o teste puro**
|
||||
|
||||
Fixture deve provar três invariantes em uma única lista: featured permanece, `featured: false` sai, e estado de demo não filtra card.
|
||||
|
||||
```ts
|
||||
const result = getFeaturedProjects(snapshot);
|
||||
expect(result.map(({ id }) => id)).toEqual(
|
||||
portfolio.projects.filter(({ featured }) => featured !== false).map(({ id }) => id),
|
||||
);
|
||||
expect(result.find(({ id }) => id === "sev-modaintima")?.demoAvailability).toBe("available");
|
||||
expect(result.find(({ id }) => id === "twins-lab")?.demoAvailability).toBe("unknown");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Atualizar o teste do loader sem apagar o baseline do usuário**
|
||||
|
||||
Produção:
|
||||
|
||||
```ts
|
||||
await expect(loadFeaturedProjects()).resolves.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ id: "sev-modaintima" })]),
|
||||
);
|
||||
expect(createProjectHealthSnapshot).not.toHaveBeenCalled();
|
||||
```
|
||||
|
||||
Desenvolvimento com snapshot antigo:
|
||||
|
||||
```ts
|
||||
expect(createProjectHealthSnapshot).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: "sev-modaintima", demoAvailability: "available" }),
|
||||
]));
|
||||
```
|
||||
|
||||
Adicionar falha de refresh: a promise rejeitada não rejeita o loader nem esvazia a lista.
|
||||
|
||||
- [ ] **Step 3: Rodar ambos os testes e observar a quebra do contrato antigo**
|
||||
|
||||
```bash
|
||||
pnpm exec jest lib/project-health.test.ts lib/portfolio-data.test.ts lib/portfolio-data.load.test.ts --runInBand
|
||||
```
|
||||
|
||||
Expected: falha nas expectativas “sempre retorna featured”.
|
||||
|
||||
- [ ] **Step 4: Implementar projeção e loader**
|
||||
|
||||
`getFeaturedProjects` faz um único `filter(featured !== false)` e um `map`; `loadFeaturedProjects` lê snapshot uma vez. Em produção retorna a projeção imediatamente. Em desenvolvimento, atualiza somente se snapshot ausente/antigo, envolve o refresh em `try/catch` e usa snapshot anterior em falha.
|
||||
|
||||
- [ ] **Step 5: Migrar a home**
|
||||
|
||||
Em `app/page.tsx`:
|
||||
|
||||
```tsx
|
||||
import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<CinematicHome
|
||||
projects={await loadFeaturedProjects()}
|
||||
experiences={portfolio.experiences}
|
||||
skillCategories={portfolio.skills.technical}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Remover `revalidate = 300`; não adicionar outro cron/cache para saúde.
|
||||
|
||||
- [ ] **Step 6: Checkpoint do loader**
|
||||
|
||||
Run o comando do passo 3. Expected: todos passam; mock de fetch não é chamado em produção; rejeição dev ainda retorna cards.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Apresentar disponibilidade sem remover evidência
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/Projects.tsx`
|
||||
- Create: `app/components/Projects.test.tsx`
|
||||
- Modify: `app/components/story/notebookProjects.ts`
|
||||
- Modify: `app/components/story/notebookProjects.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PortfolioProjectView[]`.
|
||||
- Produces: card sempre visível e CTA de demo acessível por estado.
|
||||
|
||||
- [ ] **Step 1: Escrever testes de UI por papel e nome acessível**
|
||||
|
||||
Cobrir três cards:
|
||||
|
||||
```tsx
|
||||
expect(screen.getByRole("heading", { name: "Demo disponível" })).toBeVisible();
|
||||
expect(screen.getByRole("link", { name: "Abrir projeto" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://available.example",
|
||||
);
|
||||
expect(screen.getByText("Demo indisponível na última verificação")).toHaveAttribute(
|
||||
"aria-disabled",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByRole("link", { name: /Abrir projeto.*disponibilidade não verificada/i }))
|
||||
.toHaveAttribute("href", "https://unknown.example");
|
||||
```
|
||||
|
||||
O teste não inspeciona classes, props de motion ou contagem de wrappers.
|
||||
|
||||
- [ ] **Step 2: Rodar teste vermelho**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/Projects.test.tsx --runInBand
|
||||
```
|
||||
|
||||
Expected: falha porque `Projects` ainda trata qualquer `demoUrl` como navegável e não anota estado.
|
||||
|
||||
- [ ] **Step 3: Implementar CTA por estado**
|
||||
|
||||
- `available`: link atual.
|
||||
- `unavailable`: `span` visualmente equivalente, `aria-disabled="true"`, sem `href`, `target` ou click.
|
||||
- `unknown`: link configurado, com texto auxiliar acessível “Disponibilidade não verificada”.
|
||||
- `none`: sem CTA.
|
||||
|
||||
Não esconder `article`, título, descrição, stack, GitHub ou status.
|
||||
|
||||
- [ ] **Step 4: Adaptar o seletor do notebook sem torná-lo dependente de saúde**
|
||||
|
||||
`selectNotebookProjects` continua usando screenshot local + `demoUrl`; aceita `PortfolioProjectView[]` estruturalmente e não filtra por `demoAvailability`.
|
||||
|
||||
- [ ] **Step 5: Checkpoint da apresentação**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/Projects.test.tsx app/components/story/notebookProjects.test.ts lib/portfolio-data.test.ts --runInBand
|
||||
```
|
||||
|
||||
Expected: todos passam e o fixture indisponível continua presente.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Remover bloqueadores globais e corrigir foco/reduced motion
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/layout.tsx`
|
||||
- Modify: `app/components/cinematic/CinematicOverlays.tsx`
|
||||
- Modify: `app/globals.css`
|
||||
- Delete: `app/components/cinematic/PageIntro.tsx`
|
||||
- Delete: `app/components/cinematic/PageIntro.test.tsx`
|
||||
- Delete: `app/providers/SmoothScrollProvider.tsx`
|
||||
- Delete: `app/providers/AnimationProvider.tsx`
|
||||
- Delete: `app/providers/AnimationProvider.test.tsx`
|
||||
- Delete: `app/lib/animations/config.ts`
|
||||
- Delete: `app/lib/animations/variants.ts`
|
||||
- Delete: `app/components/RouteTransition.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: layout server-first sem splash, Lenis, contexto ou transição global.
|
||||
|
||||
- [ ] **Step 1: Repetir referências e remover `PageIntro` do overlay**
|
||||
|
||||
`CinematicOverlays` não importa nem renderiza o portal. Excluir componente e teste no mesmo passo.
|
||||
|
||||
- [ ] **Step 2: Achatar o body do layout**
|
||||
|
||||
Preservar JSON-LD, skip link, Header, overlays e analytics na ordem abaixo:
|
||||
|
||||
```tsx
|
||||
<body>
|
||||
<script type="application/ld+json">{safeJsonLd(jsonLd)}</script>
|
||||
<a className="skip-link" href="#main-content">Pular para o conteúdo</a>
|
||||
<Header />
|
||||
<CinematicOverlays />
|
||||
<div id="main-content" tabIndex={-1}>{children}</div>
|
||||
<GoogleAnalytics />
|
||||
</body>
|
||||
```
|
||||
|
||||
Não tocar no objeto `metadata` nem no grafo JSON-LD.
|
||||
|
||||
- [ ] **Step 3: Excluir infraestrutura sem consumidores**
|
||||
|
||||
Apagar os providers, config/variants e `RouteTransition`; repetir busca pelos nomes e exigir zero resultado em código vivo.
|
||||
|
||||
- [ ] **Step 4: Corrigir cascata de links**
|
||||
|
||||
Mover `a { color: inherit; }` para dentro de `@layer base`, junto do reset, para as utilities da 404 prevalecerem.
|
||||
|
||||
- [ ] **Step 5: Adicionar fallback reduced motion global**
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Não usar `display: none` para conteúdo.
|
||||
|
||||
- [ ] **Step 6: Checkpoint de shell no browser**
|
||||
|
||||
Iniciar a app pelo processo supervisionado, abrir `/`, focar o primeiro controle com `Tab` e confirmar que o skip link aparece imediatamente e move foco para `#main-content`. Abrir uma rota inexistente e confirmar visualmente: CTA primário branco sobre azul; CTA secundário slate e hover branco. Emular reduced motion e confirmar conteúdo completo sem splash/smooth scroll. Console sem erro de provider/hidratação.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Retirar GSAP redundante da home
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/cinematic/CinematicHome.tsx`
|
||||
- Modify: `app/components/cinematic/CinematicHome.test.tsx`
|
||||
- Delete: `app/components/cinematic/CinematicSection.tsx`
|
||||
- Delete: `app/components/story/ReelDirector.tsx`
|
||||
- Delete: `app/hooks/useScrollReveal.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: mesma ordem sem wrapper de revelação ou scanner global.
|
||||
|
||||
- [ ] **Step 1: Atualizar teste de integração**
|
||||
|
||||
Manter somente o contrato útil: exatamente uma história de notebook e `About` depois dela. Remover mocks/asserções de `ReelDirector` e `CinematicSection`.
|
||||
|
||||
- [ ] **Step 2: Simplificar composição**
|
||||
|
||||
Remover `ReelDirector` e renderizar `About`, `Skills`, `Experience`, `Projects`, `LighthouseMetrics`, `WorkModalities` e `Contact` diretamente. Os próprios componentes mantêm Motion/reduced motion.
|
||||
|
||||
- [ ] **Step 3: Excluir os três módulos sem consumidores**
|
||||
|
||||
Apagar `CinematicSection`, `ReelDirector` e `useScrollReveal`. GSAP deve permanecer importado somente por `NotebookStory` e nenhum caminho morto.
|
||||
|
||||
- [ ] **Step 4: Checkpoint de ordem**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/cinematic/CinematicHome.test.tsx app/components/story/NotebookStory.test.tsx --runInBand
|
||||
```
|
||||
|
||||
Expected: `PASS`; home mantém hero → notebook → about → demais seções.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Trocar efeitos contínuos evitáveis por CSS/SVG
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `app/components/cinematic/FilmGrain.tsx`
|
||||
- Rewrite: `app/components/cinematic/ScrollVelocityText.tsx`
|
||||
- Rewrite: `app/components/cinematic/SectionDivider.tsx`
|
||||
- Modify: `app/components/cinematic/CinematicOverlays.tsx`
|
||||
- Modify: `app/components/cinematic/CinematicHome.tsx`
|
||||
- Modify: `app/globals.css`
|
||||
- Delete: `app/components/cinematic/CustomCursor.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- `ScrollVelocityText` passa a consumir `direction?: "left" | "right"` no lugar de `baseVelocity`.
|
||||
- `SectionDivider` mantém `variant` para não espalhar branches no caller.
|
||||
|
||||
- [ ] **Step 1: Remover cursor customizado**
|
||||
|
||||
Excluir import/render dinâmico e o arquivo. O cursor nativo volta a operar; retirar qualquer regra global `cursor: none` junto com o componente.
|
||||
|
||||
- [ ] **Step 2: Fazer grain sem cliente/RAF**
|
||||
|
||||
`FilmGrain` retorna somente:
|
||||
|
||||
```tsx
|
||||
export default function FilmGrain() {
|
||||
return <div aria-hidden="true" className="film-grain" />;
|
||||
}
|
||||
```
|
||||
|
||||
CSS usa textura estática/repeating gradients, `pointer-events: none`, posição fixa e a opacidade atual. Reduced motion/coarse pointer pode ocultar apenas esse ornamento.
|
||||
|
||||
- [ ] **Step 3: Fazer marquee em CSS**
|
||||
|
||||
Remover `useAnimationFrame`, `useScroll`, springs, resize state e Motion. Renderizar cópias suficientes em dois trilhos sem estado; `direction` seleciona `animate-marquee` ou `animate-marquee-reverse`. Migrar três callers de `baseVelocity` para direção explícita.
|
||||
|
||||
- [ ] **Step 4: Fazer divisor sem canvas**
|
||||
|
||||
- `gradient`: duas linhas CSS; animação existente.
|
||||
- `pulse`: cinco pontos CSS.
|
||||
- `waveform`: SVG estático com três paths, `aria-hidden="true"`.
|
||||
|
||||
Nenhum branch chama `requestAnimationFrame`.
|
||||
|
||||
- [ ] **Step 5: Consolidar tokens tocados**
|
||||
|
||||
Usar `--slash: #ccff00`, `--ink` e `--paper` como fonte única nas novas regras; remover apenas keyframes/classes que ficaram sem consumidores. Não reformatar o stylesheet nem copiar o arquivo da raiz.
|
||||
|
||||
- [ ] **Step 6: Checkpoint visual/performance**
|
||||
|
||||
No browser, verificar desktop e mobile: marquee legível, divisores presentes, grain sutil, cursor nativo, nenhum layout shift. Com Performance/console, confirmar ausência de RAF originado em `FilmGrain`, `ScrollVelocityText` e `SectionDivider`.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Pausar renderers restantes fora de uso
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/story/NotebookStory.tsx`
|
||||
- Modify: `app/components/story/NotebookStory.test.tsx`
|
||||
- Modify: `app/components/cinematic/ControlRoomGlobe.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `isVisible = sectionIntersects && !document.hidden` para o Canvas do notebook.
|
||||
- Globo mantém `intersectsRef` separado de `document.hidden`.
|
||||
|
||||
- [ ] **Step 1: Escrever teste de Page Visibility para notebook**
|
||||
|
||||
Capturar a prop `isVisible` do mock dinâmico. Após `Object.defineProperty(document, "hidden", { value: true, configurable: true })` e `visibilitychange`, esperar `false`; ao voltar a `false`, o valor depende novamente da interseção.
|
||||
|
||||
- [ ] **Step 2: Implementar estado combinado no notebook**
|
||||
|
||||
Não duplicar timeline. Adicionar listener `visibilitychange`, limpar no unmount e passar a conjunção ao `NotebookStage3D`.
|
||||
|
||||
- [ ] **Step 3: Corrigir o globo**
|
||||
|
||||
O callback do Intersection Observer altera apenas `intersectsRef`. O `onRender` retorna cedo quando `!intersectsRef.current || document.hidden`; `visibilitychange` não sobrescreve estado de interseção.
|
||||
|
||||
- [ ] **Step 4: Checkpoint de pausa**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/story/NotebookStory.test.tsx app/components/story/NotebookStage3D.test.tsx --runInBand
|
||||
```
|
||||
|
||||
No browser, rolar notebook/globo para fora, ocultar/mostrar aba e verificar que não há salto nem erro WebGL ao retornar.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Reduzir Hero à variante ativa
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/Hero.tsx`
|
||||
- Modify: `app/components/Hero.test.tsx`
|
||||
- Modify: `app/components/Hero.reducedMotion.test.tsx`
|
||||
- Modify: `app/components/cinematic/HeroScene.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Hero sem prop/seleção de variante; conteúdo command-center é constante.
|
||||
|
||||
- [ ] **Step 1: Ajustar testes para o contrato visível**
|
||||
|
||||
Remover spy de `Math.random`, mock/asserção de `variant` e inspeção `data-initial`/`data-animate`. Manter heading, CTA WhatsApp, link de cases, CV, links sociais e ausência de 3D em reduced motion.
|
||||
|
||||
- [ ] **Step 2: Podar o modelo de variantes**
|
||||
|
||||
Trocar `HERO_VARIANTS[0]` por `HERO_CONTENT` sem `weight`, `scene` e `layout`. Remover cinco objetos inalcançáveis, `LAYOUT_CLASSES`, layouts alternativos e todos os branches de `SideVisual`; manter diretamente starfield, globo e telemetria.
|
||||
|
||||
- [ ] **Step 3: Simplificar `HeroScene`**
|
||||
|
||||
Remover união `HeroSceneVariant`, prop `variant` e `void variant`; caller passa somente `isVisible`.
|
||||
|
||||
- [ ] **Step 4: Checkpoint da hero**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/Hero.test.tsx app/components/Hero.reducedMotion.test.tsx --runInBand
|
||||
```
|
||||
|
||||
Expected: conteúdo e ações passam; nenhum teste depende de branch morto.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Excluir código morto e podar testes de implementação
|
||||
|
||||
**Files:**
|
||||
- Delete: grupos listados em “Código morto e testes sem valor”.
|
||||
- Create: `app/components/Experience.test.tsx`.
|
||||
- Modify: `app/components/Footer.test.tsx`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Contact` continua como formulário ativo; `Footer` e `Experience` mantêm APIs atuais.
|
||||
- Produces: zero importadores órfãos e testes comportamentais pequenos.
|
||||
|
||||
- [ ] **Step 1: Excluir caminho antigo de contato**
|
||||
|
||||
Confirmar que somente os próprios arquivos referenciam `ContactForm`, `ContactInfo` e `usePortfolioData`; apagar os três. Não alterar `Contact.tsx`.
|
||||
|
||||
- [ ] **Step 2: Excluir CompanyLogos e testes**
|
||||
|
||||
Apagar componente, `CompanyLogos.test.tsx`, `CompanyLogos.altText.pbt.test.tsx` e `CompanyLogos.reducedMotion.pbt.test.tsx`. Não apagar logos genéricos usados por `Logo`.
|
||||
|
||||
- [ ] **Step 3: Excluir biblioteca demo/magicui**
|
||||
|
||||
Apagar os seis componentes da raiz de `components/`, os nove de `components/magicui/` e `components.json`. Confirmar que `@/lib/utils` continua consumido por `ControlRoomGlobe` e `TelemetryStrip`.
|
||||
|
||||
- [ ] **Step 4: Excluir cinemáticos sem importadores**
|
||||
|
||||
Apagar `FloatingElements`, `HomeButton`, `HorizontalScroll` e `CinematicText`.
|
||||
|
||||
- [ ] **Step 5: Substituir PBTs por dois testes de Experience**
|
||||
|
||||
Excluir os três `Experience.*.pbt.test.tsx`. Novo teste usa fixture fixa e verifica:
|
||||
|
||||
```ts
|
||||
expect(screen.queryByRole("heading", { name: "Oculta" })).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole("article").map((article) =>
|
||||
within(article).getByRole("heading", { level: 3 }).textContent,
|
||||
)).toEqual(["Mais recente", "Mais antiga"]);
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Remover PBTs redundantes do Footer**
|
||||
|
||||
Excluir os três arquivos `Footer.*.pbt.test.tsx`. Em `Footer.test.tsx`, manter contato, navegação, nomes acessíveis, `target`/`rel` e ano; remover somente teste que fixa `.ds-grid-2`, quantidade de cards e classes.
|
||||
|
||||
- [ ] **Step 7: Checkpoint de corte**
|
||||
|
||||
```bash
|
||||
pnpm exec tsc --noEmit
|
||||
pnpm exec jest app/components/Experience.test.tsx app/components/Footer.test.tsx --runInBand
|
||||
```
|
||||
|
||||
Expected: TypeScript sem import quebrado; testes comportamentais passam.
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Fixar dependências, runtime e deploy
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
- Modify: `tailwind.config.js`
|
||||
- Modify: `nixpacks.toml`
|
||||
- Modify: `jest.setup.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Node `22.x`, pnpm `9.15.9`, Next `16.3.0`, scripts `typecheck` e versões exatas.
|
||||
|
||||
- [ ] **Step 1: Fixar manifesto**
|
||||
|
||||
Adicionar:
|
||||
|
||||
```json
|
||||
"engines": { "node": "22.x" },
|
||||
"packageManager": "[email protected]"
|
||||
```
|
||||
|
||||
Adicionar script:
|
||||
|
||||
```json
|
||||
"typecheck": "tsc --noEmit"
|
||||
```
|
||||
|
||||
Fixar às versões resolvidas do spec; mover `@types/three` para dev e `@types/node` para `22.20.1`.
|
||||
|
||||
- [ ] **Step 2: Remover dependências sem consumidores**
|
||||
|
||||
Remover do manifesto: `@gsap/react`, `class-variance-authority`, `lenis`, `ogl`, `tailwindcss-animate`, `tw-animate-css`, `fast-check`, `@types/resize-observer-browser`, `resize-observer-polyfill`.
|
||||
|
||||
Manter `@react-three/drei`, `@react-three/fiber`, `three`, `cobe`, `gsap`, `motion`, `clsx`, `tailwind-merge`, Jest e Testing Library.
|
||||
|
||||
- [ ] **Step 3: Retirar plugin e mock órfãos**
|
||||
|
||||
Em `tailwind.config.js`, deixar `plugins: []`. Em `jest.setup.ts`, remover import de `./__mocks__/resizeObserver` somente se o mock nativo definido no arquivo cobre todos os testes; apagar `__mocks__/resizeObserver.js` se ficar sem importador.
|
||||
|
||||
- [ ] **Step 4: Regenerar lock com a versão fixada**
|
||||
|
||||
Ativar pnpm 9.15.9 via Corepack e executar:
|
||||
|
||||
```bash
|
||||
corepack prepare [email protected] --activate
|
||||
pnpm install --lockfile-only
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
Expected: `pnpm-lock.yaml` sem seção `overrides`; Next resolve `16.3.0`; manifesto e lock concordam.
|
||||
|
||||
- [ ] **Step 5: Corrigir cadeias auditadas sem force/override**
|
||||
|
||||
Executar as auditorias separadas. Se uma cadeia direta ainda resolver versão vulnerável, atualizar o pacote direto que a introduz para a menor versão compatível corrigida, fixar essa versão e repetir os três comandos do passo 4.
|
||||
|
||||
```bash
|
||||
pnpm audit --prod --audit-level=high
|
||||
pnpm audit --dev --audit-level=high
|
||||
```
|
||||
|
||||
Expected: ambos código zero. É proibido usar `--force`, ignore ou override para obter verde.
|
||||
|
||||
- [ ] **Step 6: Congelar Nixpacks**
|
||||
|
||||
Manter `nodejs_22` e `pnpm-9_x`; trocar somente install por:
|
||||
|
||||
```toml
|
||||
[phases.install]
|
||||
cmds = ["pnpm install --frozen-lockfile"]
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Checkpoint de toolchain**
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
Expected: instalação não altera lock, TypeScript e Biome encerram em zero. Corrigir diagnósticos nos arquivos responsáveis; não alterar `biome.json` para desligar regras.
|
||||
|
||||
---
|
||||
|
||||
### Task 11: Implementar consentimento e corrigir headers/cache
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/GoogleAnalytics.tsx`
|
||||
- Create: `app/components/GoogleAnalytics.test.tsx`
|
||||
- Modify: `next.config.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Storage key: `evandro.analytics-consent.v1`.
|
||||
- Values persistidos: `granted | denied`.
|
||||
- GA ID permanece `G-QTT9QT0T3S`.
|
||||
|
||||
- [ ] **Step 1: Escrever testes de consentimento**
|
||||
|
||||
Testes devem provar:
|
||||
|
||||
```ts
|
||||
expect(screen.queryByTestId("google-analytics-script")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Aceitar analytics" })).toBeVisible();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Aceitar analytics" }));
|
||||
expect(localStorage.getItem("evandro.analytics-consent.v1")).toBe("granted");
|
||||
expect(screen.getByTestId("google-analytics-script")).toHaveAttribute(
|
||||
"src",
|
||||
"https://www.googletagmanager.com/gtag/js?id=G-QTT9QT0T3S",
|
||||
);
|
||||
```
|
||||
|
||||
Adicionar recusa sem script e revogação que grava `denied`, define `ga-disable-G-QTT9QT0T3S` e expira cookies `_ga`/`_ga_*`.
|
||||
|
||||
- [ ] **Step 2: Rodar teste vermelho**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/GoogleAnalytics.test.tsx --runInBand
|
||||
```
|
||||
|
||||
Expected: falha porque loader atual monta GA sem decisão e não oferece controles.
|
||||
|
||||
- [ ] **Step 3: Implementar UI e loader no arquivo existente**
|
||||
|
||||
Inicializar estado após mount lendo somente a chave versionada. Sem decisão, renderizar um diálogo não modal acessível com descrição curta e aceitar/recusar. Após escolha, manter botão de preferências. Montar `next/script` direto apenas em `granted`; advertising consent fica sempre denied.
|
||||
|
||||
- [ ] **Step 4: Implementar revogação completa alcançável**
|
||||
|
||||
Ao revogar:
|
||||
|
||||
```ts
|
||||
window["ga-disable-G-QTT9QT0T3S"] = true;
|
||||
window.gtag?.("consent", "update", {
|
||||
analytics_storage: "denied",
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
});
|
||||
```
|
||||
|
||||
Expirar somente cookies cujos nomes começam com `_ga`, para path `/` e domínio atual quando aplicável. Não apagar storage/cookies alheios.
|
||||
|
||||
- [ ] **Step 5: Remover rewrites e cache incorreto**
|
||||
|
||||
Excluir `rewrites()` inteiro de `next.config.ts`. Remover headers customizados `immutable` de `/static`, `/fonts`, `/assets`, `/logos`.
|
||||
|
||||
- [ ] **Step 6: Ajustar CSP com risco controlado**
|
||||
|
||||
Manter `Content-Security-Policy-Report-Only`. Incluir `default-src 'self'` e `form-action 'self'`; construir `script-src` com `unsafe-eval` somente em desenvolvimento. Manter `unsafe-inline` e origens oficiais do Google. Não criar nonce/proxy nesta rodada.
|
||||
|
||||
- [ ] **Step 7: Checkpoint automatizado e browser**
|
||||
|
||||
```bash
|
||||
pnpm exec jest app/components/GoogleAnalytics.test.tsx --runInBand
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
No browser de produção local: limpar a chave/cookies; confirmar zero request Google antes de escolher; recusar e recarregar, ainda zero; aceitar e observar um único carregamento direto; revogar e confirmar flag/consent denied e cookies acessíveis removidos. Conferir console sem violações que quebrem a app.
|
||||
|
||||
---
|
||||
|
||||
### Task 12: Criar CI, atualizar documentação e fechar a iniciativa
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/ci.yml`
|
||||
- Modify: `README.md`
|
||||
- Move/replace: `docs/project-health-cron.md` → `docs/project-health.md`
|
||||
|
||||
**Interfaces:**
|
||||
- CI usa scripts reais de `package.json` e lock final.
|
||||
- Docs descrevem somente o comportamento verificado nas tarefas 1–11.
|
||||
|
||||
- [ ] **Step 1: Criar workflow único**
|
||||
|
||||
Conteúdo operacional:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm test -- --runInBand
|
||||
- run: pnpm build
|
||||
- run: pnpm audit --prod --audit-level=high
|
||||
- run: pnpm audit --dev --audit-level=high
|
||||
```
|
||||
|
||||
`pnpm/action-setup` lê `packageManager`; não duplicar versão no workflow.
|
||||
|
||||
- [ ] **Step 2: Reescrever README com fatos do repositório**
|
||||
|
||||
Atualizar badge/stack para Next 16.3.0; remover Lenis, demos apagados e PBTs; registrar Node 22, pnpm 9.15.9, quick start, todos os scripts existentes, App Router, conteúdo estático, notebook 3D progressivo, consentimento, CI e Nixpacks frozen. Não criar seção de resultados comerciais nem promessas de performance não medidas.
|
||||
|
||||
- [ ] **Step 3: Substituir documentação de cron**
|
||||
|
||||
Remover `docs/project-health-cron.md` e criar `docs/project-health.md` com:
|
||||
|
||||
- cards derivados de `featured`, nunca de saúde;
|
||||
- estados `available`, `unavailable`, `unknown`, `none`;
|
||||
- produção sem fetch externo durante render;
|
||||
- atualização dev em memória quando snapshot está antigo;
|
||||
- `pnpm check:projects` como atualização manual atômica opcional;
|
||||
- `data/project-health.json` como última observação versionada, não requisito de conteúdo.
|
||||
|
||||
Não editar o snapshot ao documentá-lo.
|
||||
|
||||
- [ ] **Step 4: Rodar gate completo uma única vez**
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm lint
|
||||
pnpm typecheck
|
||||
pnpm test -- --runInBand
|
||||
pnpm build
|
||||
pnpm audit --prod --audit-level=high
|
||||
pnpm audit --dev --audit-level=high
|
||||
```
|
||||
|
||||
Expected: todos encerram com código zero; install não modifica lock; build lista as rotas atuais.
|
||||
|
||||
- [ ] **Step 5: Smoke browser final**
|
||||
|
||||
Exercitar `/`, `/cases`, `/servicos`, `/curriculo` e uma 404 em desktop e viewport móvel. Confirmar projetos, foco, menu, notebook/fallback, formulário/WhatsApp, reduced motion, consentimento e ausência de erro de console. Não instalar runner de browser.
|
||||
|
||||
- [ ] **Step 6: Conferir arquivos protegidos e corte limpo**
|
||||
|
||||
Por leitura/comparação com snapshots iniciais, confirmar:
|
||||
|
||||
- `data/project-health.json` inalterado;
|
||||
- `globals.css` da raiz inalterado;
|
||||
- alterações em `lib/portfolio-data.ts` e seu teste preservam o refresh dev existente enquanto removem fail-closed;
|
||||
- nenhum import aponta para arquivo excluído;
|
||||
- nenhum artifact temporário, log, relatório ou cache foi criado no repo.
|
||||
|
||||
## Auto-revisão de cobertura do plano
|
||||
|
||||
| Requisito aprovado | Cobertura |
|
||||
| --- | --- |
|
||||
| Cards sempre visíveis; health não remove; dev atualiza; prod sem cron | Tasks 1–3 e 12 |
|
||||
| Next 16.3.0, advisories, sem force/overrides, Node 22, pnpm 9, frozen lock | Task 10 |
|
||||
| Remover PageIntro, corrigir 404, reduced motion, pausar loops | Tasks 4, 6 e 7 |
|
||||
| Código morto, Hero única, PBTs, deps órfãs, Biome verde | Tasks 5, 8, 9 e 10 |
|
||||
| Tirar Lenis/GSAP global, reduzir JS/long tasks, tokens/cache | Tasks 4–6, 10 e 11 |
|
||||
| CI com install/lint/tsc/test/build/audit; sem browser pesado | Task 12 |
|
||||
| Sem bypass de ad blocker; consentimento/revogação; CSP compatível | Task 11 |
|
||||
| README/deploy/project-health grounded | Task 12 |
|
||||
| Preservar identidade e não inventar claims | Restrições globais e smoke final |
|
||||
| Não integrar SEO externo nesta rodada | Restrições globais e não-objetivo do spec |
|
||||
| Preservar mudanças do usuário e não usar worktree/commits | Restrições globais, rollback do spec e Task 12 |
|
||||
|
||||
A auto-revisão confirmou que todas as interfaces, ordens, comandos, estados e critérios necessários estão definidos. Não há passo de commit. A execução termina somente após o gate completo e o smoke real.
|
||||
Reference in new issue
Block a user