36 KiB
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.jsone oglobals.cssnão rastreado da raiz sem qualquer edição. - Evoluir, sem restaurar, o conteúdo atual de
lib/portfolio-data.tselib/portfolio-data.load.test.ts. - Editar somente
app/globals.css; nunca importar, mover ou apagar oglobals.cssda 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.tsxe 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/ecomponents.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
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:
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:
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:
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:
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:
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.
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:
await expect(loadFeaturedProjects()).resolves.toEqual(
expect.arrayContaining([expect.objectContaining({ id: "sev-modaintima" })]),
);
expect(createProjectHealthSnapshot).not.toHaveBeenCalled();
Desenvolvimento com snapshot antigo:
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
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:
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:
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
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:spanvisualmente equivalente,aria-disabled="true", semhref,targetou 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
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
PageIntrodo 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:
<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
@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
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:
-
ScrollVelocityTextpassa a consumirdirection?: "left" | "right"no lugar debaseVelocity. -
SectionDividermantémvariantpara 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:
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.hiddenpara o Canvas do notebook. -
Globo mantém
intersectsRefseparado dedocument.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
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
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:
Contactcontinua como formulário ativo;FootereExperiencemantê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:
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
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, pnpm9.15.9, Next16.3.0, scriptstypechecke versões exatas. -
Step 1: Fixar manifesto
Adicionar:
"engines": { "node": "22.x" },
"packageManager": "[email protected]"
Adicionar script:
"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:
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.
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:
[phases.install]
cmds = ["pnpm install --frozen-lockfile"]
- Step 7: Checkpoint de toolchain
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:
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
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:
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
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.jsone lock final. -
Docs descrevem somente o comportamento verificado nas tarefas 1–11.
-
Step 1: Criar workflow único
Conteúdo operacional:
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:projectscomo atualização manual atômica opcional;data/project-health.jsoncomo ú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
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.jsoninalterado;globals.cssda raiz inalterado;- alterações em
lib/portfolio-data.tse 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.