From d157554f0eddd31aa53f9d37b08ba6d94ffa2c01 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sun, 23 Aug 2026 14:59:30 -0300 Subject: [PATCH] =?UTF-8?q?docs(plans):=20plano=20de=20inten=C3=A7=C3=A3o?= =?UTF-8?q?=20OPS80/OPS79/OPS81=20=E2=80=94=20cutoff=20da=20URL=20pt.jorge?= =?UTF-8?q?solla.com.br=20(desativar=20=E2=86=92=20migrar=20=E2=86=92=20re?= =?UTF-8?q?direcionar)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ops79-ultima-migracao-dados-campanha.md | 73 +++++++++++++++++++ .../ops80-cutoff-desativar-url-antiga.md | 71 ++++++++++++++++++ .../ops81-cutoff-redirecionar-url-antiga.md | 72 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 docs/plans/ops79-ultima-migracao-dados-campanha.md create mode 100644 docs/plans/ops80-cutoff-desativar-url-antiga.md create mode 100644 docs/plans/ops81-cutoff-redirecionar-url-antiga.md diff --git a/docs/plans/ops79-ultima-migracao-dados-campanha.md b/docs/plans/ops79-ultima-migracao-dados-campanha.md new file mode 100644 index 00000000..65290c5d --- /dev/null +++ b/docs/plans/ops79-ultima-migracao-dados-campanha.md @@ -0,0 +1,73 @@ +# Última migração de dados em produção: plataforma antiga → nova (vertical campanha) + +Status: rascunho +Atualizado em: 2026-08-23 +Issue: #797 +Priority: P1 +Impeccable: A — sem UI +Rascunho UI: N/A — sem UI +Appetite: ~2–3 dias eng; um outcome verificável (nenhum dado de operação de campanha viver só na plataforma antiga antes do desligamento) +Responsável: — + +## Intenção + +Durante a transição, o site continuou vivo em DUAS plataformas: a antiga `pt.jorgesolla.com.br` (Vercel + banco Neon) e a nova `jorgesolla1313.com.br` (homeserver, banco próprio). O time de campanha seguiu usando `/campanha` na URL antiga; depois do OPS51 (migração Neon→homeserver, 2026-08-17), qualquer dado gravado na plataforma antiga ficou órfão da nova. **OPS80 desativa a URL antiga primeiro, congelando a fonte** (nenhuma escrita nova). Este item roda DEPOIS da desativação: trazer para o banco novo o estado final da plataforma antiga, PRINCIPALMENTE a vertical campanha (lideranças, pledges, apoiadores, atividades, usuários da campanha, etc.) — para nenhuma operação de campanha viver só na plataforma antiga no momento em que ela for desligada. + +## Persona e fluxo + +- **Persona / contexto:** coordinator e staff de campanha que operam `/campanha` diariamente em janela eleitoral ativa; engenharia/ops responsável pelo desligamento da plataforma antiga. +- **Job principal:** nenhum dado de campanha — nem um pledge, apoiador, liderança, atividade ou usuário — viver só na plataforma antiga no momento do desligamento. +- **Fluxo desejado:** OPS80 desativa a URL antiga (fonte congelada, sem escritas novas) → este item captura o estado final da plataforma antiga → o residual é reconciliado e trazido para a nova → contagens conferem (nada órfão, nada duplicado) → `/campanha` segue operando só na plataforma nova, sem perda e sem regressão → a URL antiga permanece desligada. +- **Anti-goals de produto:** NÃO re-migrar tudo por desconfiança; NÃO duplicar municípios/seed nem dados públicos já sincronizados; NÃO virar "conserto" de divergências históricas; NÃO criar superfície nova de edição de dados (isso é planilha-modo). + +## Objetivo e aceite + +- Após o item, todos os dados de operação de campanha existentes na plataforma antiga existem e estão íntegros na nova — contagens conferem e nenhum registro ficou para trás. +- Nenhuma operação de campanha registrada na plataforma antiga (pledges, apoiadores, lideranças, atividades, demandas, decisões de alocação) precisa ser refeita manualmente na nova. +- Usuários da campanha continuam acessando `/campanha` na plataforma nova sem perda de acesso (senhas/sessões preservadas ou re-emitidas sem re-cadastro). +- Guardrails: dados nominais de cidadãos (PII/LGPD) só migram com o mesmo fail-closed do app (consentimento por chave estável); municípios/semeados não são duplicados; a vertical não regride (assimetria de votos, lockdown de liderança, leitura relativa/local preservadas). + +## Dados (intenção) + +- **Vou apresentar dados?** Não — migração move dados, não apresenta superfície de decisão. +- **Decisões desbloqueadas:** nada novo que dependa de um dashboard; o que importa é que decisões JÁ existentes continuem íntegras pós-migração: (a) conta da cadeira, (b) mapas e leitura territorial, (c) pledges com `declaredVotes` vs `estimatedVotes` e seus agregados devem refletir o MESMO estado que o time via na plataforma antiga. +- **Forma:** *adiada ao plano de implementação* — aqui só a restrição de produto: o pós-migração precisa produzir a mesma leitura relativa/local que a plataforma antiga mostrava (sem "conserto" silencioso de números). + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** coleções da vertical campanha (`src/collections/`) e a área de campanha (`src/app/(campaign)/…`, `src/utilities/access/*`); precedente de tooling de dados em `scripts/` (ex.: `scripts/db-pull.mjs`, `pnpm db:pull`). +- **Precedente a olhar:** OPS51 (dump Neon→homeserver, contagens por coleção), OPS58 (sincronização de posts públicos), OPS52 (media) — docs de ops em `docs/ops/` e `docs/plans/`. +- **Risco de acoplamento:** o executor deve respeitar as guardas de dev/test (nunca apontar dev/test para prod) e o fail-closed de consentimento/LGPD — esta migração é operação manual de infra em prod, fora dos guards de dev. + +## Dependências + +- **OPS80 (duro):** a desativação/congelamento da URL antiga vem ANTES — a fonte precisa estar quiescida para que nenhum dado novo entre entre a última migração e o desligamento. +- Nenhuma dependência suave. + +## Fora de escopo + +- Cutover de DNS/hospedagem de `pt.jorgesolla.com.br` (OPS80 desativa + OPS81 redireciona). +- Migração de media — já feita em OPS52 (Garage S3); referências antigas são exceção a registrar, não re-migração. +- Cancelamento da conta Neon (pós-OPS80). +- Dados públicos já sincronizados (ex.: posts — OPS58). + +## Rabbit holes de produto + +- **Merge de delta vs reconciliação limpa.** Se alguém "só completar" com o delta pós-OPS51, uma divergência silenciosa anterior continua escondida; se reconciliar tudo, vira re-migração completa. **Corte neste item:** reconciliar a vertical inteira uma última vez (contagens vs dump OPS51 como linha de base), com o delta como piso — sem re-migrar o que já está íntegro. +- **Copiar dados públicos duplicados.** Trazer posts/assinaturas/signatures de novo criaria duplicidade com o que já foi sincronizado. **Corte:** apenas a vertical campanha é o foco; o resto só se aparecer residual real e verificável. +- **"Consertar" divergências históricas de IDs.** Em vez de aceitar que a fonte de verdade é a nova, alguém pode tentar reconciliar históricos de IDs que já divergiram. **Corte:** nenhuma correção de IDs pós-fato; o aceite é integridade de dados de campanha, não identidade perfeita entre plataformas. +- **Scope creep para re-migrar tudo.** A desconfiança pós-OPS51 pode inflar o item para "re-fazer a migração inteira". **Corte:** escopo é o residual + reconciliação da vertical campanha; fora disso, OPS80 decide. + +## Questões em aberto (produto) + +- **Qual é a base real da plataforma antiga hoje (Vercel ainda aponta para o Neon)?** **Opções:** (a) Neon — assumir que toda escrita pós-OPS51 foi no Neon e migrar esse residual; (b) outra base — a plataforma antiga já foi repontada e o gap é outro; (c) plataforma antiga inativa desde o OPS51. **Recomendação:** verificação viva (read-only, escrita de teste e leitura de volta em ambas as plataformas) ANTES do cutoff de OPS80 — sem essa verificação, a migração pode estar varrendo o banco errado. A cópia em si roda depois, sobre a fonte congelada. _(assumido — validar com engenharia no momento do desligamento)_ +- **Migrar só o delta pós-OPS51 ou reconciliar a vertical inteira?** **Opções:** (a) só delta — rápido, mas deixa divergência silenciosa possível; (b) reconciliar a vertical inteira — mais demorado, mas fecha o risco de dado órfão anterior ao OPS51; (c) delta + reconciliação por contagem. **Recomendação:** (c) — reconciliar por contagem (a mesma verificação do OPS51) com o delta como objeto da cópia. É o único caminho que dá o aceite "nada órfão, nada duplicado" sem re-migrar tudo. +- **O que fazer com senhas/sessões de usuários da campanha?** **Opções:** (a) preservar hashes e sessões como estão — nenhuma interrupção no meio da janela eleitoral; (b) exigir re-login após a migração; (c) re-emitir convites. **Recomendação:** (a) — campanha em janela eleitoral ativa; forçar re-login em massa seria a pior hora para interromper operação. Se a base de origem não for confiável para credenciais, cair em (b), nunca em (c). +- **Referências a media antiga em registros migrados?** **Opções:** (a) migrar apontando para o objeto já no Garage S3; (b) manter URL antiga e registrar exceção; (c) quebrar a referência. **Recomendação:** (a) quando houver equivalente já migrado (OPS52), (b) como exceção registrada caso contrário — nunca (c), imagem quebrada é regressão visível. + +## Referências + +- GitHub Issue #— (após `pnpm agent:register`) +- OPS51 (migração Neon→homeserver, contagens por coleção) e seu rollback documentado em `docs/ops/teqo-1313-deploy.md` +- OPS52 (media → Garage S3), OPS58 (sincronização de posts públicos), OPS80 (desativa a URL antiga — pré-requisito que congela a fonte), OPS81 (redireciona a URL antiga — roda após esta migração) +- `AGENTS.md` / `AGENTS-campaign.md` — vertical campanha, LGPD (fail-closed de consentimento), guardas de dev/test diff --git a/docs/plans/ops80-cutoff-desativar-url-antiga.md b/docs/plans/ops80-cutoff-desativar-url-antiga.md new file mode 100644 index 00000000..0f367f1a --- /dev/null +++ b/docs/plans/ops80-cutoff-desativar-url-antiga.md @@ -0,0 +1,71 @@ +# Cutoff de transição (fase 1) — desativar o acesso ao site pela URL pt.jorgesolla.com.br (congelar a fonte) + +Status: rascunho +Atualizado em: 2026-08-23 +Issue: #796 +Priority: P1 +Impeccable: A — sem UI +Rascunho UI: N/A — sem UI +Appetite: ~1 dia eng; um outcome verificável (a URL antiga deixa de servir o app — fonte congelada, sem escritas novas) +Responsável: — + +## Intenção + +A transição acabou: o site novo vive em `jorgesolla1313.com.br` (homeserver + túnel Cloudflare), mas `pt.jorgesolla.com.br` continua no ar servindo o app pela plataforma antiga (Vercel) — DUAS fontes de verdade, tráfego dividido, e qualquer dado gravado na URL antiga fica órfão da nova. **Fase 1 do cutoff (este item):** o site deixa de ser servido pela URL antiga — o que **congela a fonte** (nenhuma escrita nova na plataforma antiga). O redirecionamento NÃO entra aqui: ele acontece só DEPOIS da última migração de dados (OPS79), na fase 2 (OPS81) — decisão do humano, para ninguém cair no domínio canônico antes de ele ter os dados finais. + +## Persona e fluxo + +- **Persona / contexto:** (1) time de campanha — assessores e lideranças que ainda usam `pt.jorgesolla.com.br/campanha`; (2) ops/eng — hoje obrigada a manter e vigiar duas plataformas. +- **Job principal:** congelar a plataforma antiga — parar de servir o app pela URL antiga e impedir qualquer escrita nova — abrindo caminho para a última migração (OPS79) sobre uma fonte estável. +- **Fluxo desejado:** o cutoff desliga o app na URL antiga (fonte congelada) → OPS79 captura o estado final e traz para a nova → (fase 2, OPS81) a URL antiga passa a redirecionar para a canônica. Neste item, a URL antiga fica num estado de transição (fora do ar/sem interação), não servindo o app nem ainda redirecionando. +- **Anti-goals de produto:** não deixar a URL antiga servindo o app em qualquer pedaço; não redirecionar já (redirect é OPS81, pós-migração); não interromper o acesso do time a `/campanha` **no domínio canônico** (que segue no ar normalmente). + +## Objetivo e aceite + +- `pt.jorgesolla.com.br` deixa de servir o app — um acesso pela URL antiga **não** inicia mais sessão, formulário ou qualquer escrita (fonte congelada). +- Nenhuma escrita nova é possível via URL antiga a partir do cutoff — o gap "dados novos entre a última migração e a desativação" fica estruturalmente impossível. +- A URL antiga **não** redireciona ainda para o canônico (isso é OPS81, após OPS79) — estado de transição aceito, janela curta. +- A vertical de campanha no domínio canônico (`jorgesolla1313.com.br/campanha`) continua funcionando sem regressão. + +## Dados (intenção) + +- **Vou apresentar dados?** Não. +- **Decisões desbloqueadas:** o cutoff é uma decisão de política (uma única fonte de verdade), não de dado. +- **Forma:** *adiada ao plano de implementação* — sem restrição de produto além do acima. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** superfície que referencia `pt.jorgesolla.com.br` no repo — scripts de verificação (`scripts/check-push-chain.mjs`), testes unit/int (WebAuthn config, json mutation route, multi-phones, aiMarkdownLinks), docs históricos (ops50/51, c98-impl, revalidate-secret, push-notificacoes, c115, CUTOVER-MAIN-ONLY) e personas de agentes (`.opencode/agent/`). A desativação em si é infra de hosting (Vercel/DNS), fora do app. +- **Precedente a olhar:** histórico de OPS50 (remoção de artefatos Vercel), OPS51/OPS52 (cutover adiado, "Excluir a store do Vercel Blob — só após cutover estável"). +- **Risco de acoplamento:** a URL canônica (`NEXT_PUBLIC_SITE_URL`) já é obrigatória em prod e governa invite, calendarFeed, syncs, passwordReset e WebAuthn — o executor deve manter o app 100% canônico e não criar caminhos dependentes da URL antiga. + +## Dependências + +- **Nenhuma** — este item é o PRIMEIRO da sequência do cutoff (desativação → migração → redirect). +- OPS79 (migração) **depende** deste: a fonte precisa estar congelada antes da última migração. + +## Fora de escopo + +- Última migração de dados (OPS79) — roda depois, sobre a fonte congelada. +- Redirecionamento da URL antiga (OPS81) — só após a migração. +- Cancelamento financeiro das contas Neon/Vercel — decisão de infra pós-cutoff, fora deste item. +- Remoção de `@vercel/analytics`/`@vercel/speed-insights` — não relacionada ao domínio. +- SEO/canonical tags do site público — não é este item. + +## Rabbit holes de produto + +- **Manter a plataforma antiga viva "por precaução".** Se alguém "só completar" mantendo a Vercel no ar, seguem duas fontes de verdade e o congelamento não acontece. **Corte neste item:** a URL antiga deixa de servir o app; ponto. +- **Já redirecionar na fase 1.** Apressar o redirect deixaria o canônico (ainda sem a migração) como destino de quem vem da URL antiga — exatamente o gap de dados intermediários que o humano quer evitar. **Corte:** o redirect é OPS81, após OPS79. +- **SEO/canonical vira redesenho.** Melhorar ranking/`` do site público é outro esforço. **Corte neste item:** foco na transição de domínio; SEO é plano próprio. + +## Questões em aberto (produto) + +- **O que a URL antiga deve responder no intervalo até o redirect (OPS81)?** **Opções:** (A) estado neutro estático (hold: "site em migração", sem formulários/escritas); (B) fora do ar (não responde / erro). **Recomendação:** (A) — informa quem caiu de paraquedas sem servir o app nem aceitar escrita; janela curta, em seguida OPS79 migra e OPS81 redireciona. _(assumido — validar com produto)_ +- **Como efetivar a desativação?** **Opções:** (A) remover o domínio/alias da plataforma antiga (Vercel) e derrubar o deployment; (B) repontar DNS para estado morto. **Recomendação:** decisão do executor; o requisito é o app não servir a URL antiga e nenhuma escrita ser possível. + +## Referências + +- Estado vivo (2026-08-23): `pt.jorgesolla.com.br` responde 200 via Vercel; `jorgesolla1313.com.br` responde 200 via Cloudflare (túnel do homeserver); sem host-redirect no app. +- Superfície que referencia a URL antiga no repo: `scripts/check-push-chain.mjs:13,47,49`; `tests/unit/campaignWebAuthnConfig.unit.spec.ts:17-52`; `tests/unit/campaignJsonMutationRoute.unit.spec.ts:27,54`; `tests/int/campaignMultiPhones.int.spec.ts:336`; `tests/unit/aiMarkdownLinks.unit.spec.ts:66`; `.opencode/agent/designer-campanha-solla.md:9,21`; `.opencode/agent/solla-comunicacao.md:19`. +- Docs históricos do cutover: `docs/plans/ops50*`, `ops51*`, `ops52-...-impl.md` (linha 104 — Vercel Blob pós-cutover), `CUTOVER-MAIN-ONLY.md`. +- `AGENTS.md` (Teqo context) — OPS50 removeu artefatos Vercel; `NEXT_PUBLIC_SITE_URL` obrigatória em prod via `getCampaignInviteBaseURL`. diff --git a/docs/plans/ops81-cutoff-redirecionar-url-antiga.md b/docs/plans/ops81-cutoff-redirecionar-url-antiga.md new file mode 100644 index 00000000..292a027c --- /dev/null +++ b/docs/plans/ops81-cutoff-redirecionar-url-antiga.md @@ -0,0 +1,72 @@ +# Cutoff de transição (fase 2) — redirecionar pt.jorgesolla.com.br/campanha para jorgesolla1313.com.br/campanha + +Status: rascunho +Atualizado em: 2026-08-23 +Issue: #798 +Priority: P1 +Impeccable: A — sem UI +Rascunho UI: N/A — sem UI +Appetite: ~0,5–1 dia eng; um outcome verificável (a URL antiga termina na canônica, com path preservado) +Responsável: — + +## Intenção + +Depois da desativação (OPS80, que congelou a fonte) e da última migração de dados (OPS79), falta a **fase 2 do cutoff**: dar vida útil à URL antiga como mero redirecionador. Quem ainda acessar `pt.jorgesolla.com.br/campanha` — bookmarks, links de WhatsApp/QR, material impresso — cai direto na vertical de campanha do domínio canônico `jorgesolla1313.com.br/campanha`. O redirecionamento vem SÓ depois da migração, por decisão do humano: assim ninguém cai no canônico antes de ele ter os dados finais (nenhum "dado intermediário" fica de fora da leitura). + +## Persona e fluxo + +- **Persona / contexto:** (1) time de campanha — assessores e lideranças com bookmarks/links antigos, em campo, acessando `/campanha` no celular; (2) visitante público com link antigo (WhatsApp/QR/email antigo); (3) ops/eng — fecha o cutoff com uma única fonte de verdade. +- **Job principal:** quem ainda usar a URL antiga chega ao destino certo na canônica — especialmente `/campanha` — sem página morta e sem dados faltando. +- **Fluxo desejado:** usuário abre/digita a URL antiga → é redirecionado para a URL canônica preservando o path (`/campanha` → `/campanha`) → segue navegando em `jorgesolla1313.com.br` com os dados finais já migrados. +- **Anti-goals de produto:** não deixar a URL antiga servindo o app em qualquer pedaço; não redirecionar antes da migração (a ordem desativação→migração→redirect é decisão do humano); não transformar o cutoff num redesenho de SEO/canonical. + +## Objetivo e aceite + +- `pt.jorgesolla.com.br/campanha` redireciona para `jorgesolla1313.com.br/campanha` preservando o destino — o time segue operando com links antigos, sem perder sessão de trabalho. +- Idealmente toda rota da URL antiga redireciona para a canônica com path preservado (o resto do site também deixa de ter URL morta) — `/campanha` é o caso obrigatório. +- A URL antiga continua **não** servindo o app (nenhuma escrita possível — fonte segue congelada). +- A vertical de campanha no domínio canônico continua funcionando — WebAuthn e sessão seguem válidas no host que serve a página (rpID/origin casam com o domínio canônico), sem regressão. +- Ops/eng deixa de manter a URL antiga como plataforma viva (deixa de existir decisão de "qual URL é a canônica"). + +## Dados (intenção) + +- **Vou apresentar dados?** Não. +- **Decisões desbloqueadas:** nenhuma decisão de produto depende de métrica aqui; o cutoff é uma decisão de política (uma única fonte de verdade), não de dado. +- **Forma:** *adiada ao plano de implementação* — sem restrição de produto além do acima. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** infra de hosting da URL antiga (o redirecionador roda fora do app — Cloudflare/DNS/host estático; o repo não tem host-redirect hoje, sem `src/middleware.ts`/`proxy.ts`; todos os `redirect()` são por path relativo). Superfície de repo a rever: `scripts/check-push-chain.mjs`, testes unit/int que pinam a URL antiga e docs/personas de agentes (`.opencode/agent/`). +- **Precedente a olhar:** histórico de OPS50 (remoção de artefatos Vercel), OPS51/OPS52 (cutover adiado). +- **Risco de acoplamento:** a URL canônica (`NEXT_PUBLIC_SITE_URL`) já é obrigatória em prod e governa invite, calendarFeed, syncs, passwordReset e WebAuthn — o executor deve manter o app 100% canônico e não criar caminhos dependentes da URL antiga. + +## Dependências + +- **OPS79 (dura)** — o redirecionamento só acontece DEPOIS da última migração: quem cair no canônico precisa já ver os dados finais. Sem isso, o redirect criaria o exato gap de dados intermediários que a ordem desativação→migração→redirect evita. +- OPS80 (desativação) é pré-requisito estrutural da sequência (já concluído quando este item roda). + +## Fora de escopo + +- Desativação da URL antiga (OPS80) — já feita, fase 1. +- Última migração de dados (OPS79) — já feita, entre as duas fases. +- Cancelamento financeiro das contas Neon/Vercel — decisão de infra pós-cutoff, fora deste item. +- Remoção de `@vercel/analytics`/`@vercel/speed-insights` — não relacionada ao domínio. +- SEO/canonical tags do site público — não é este item. + +## Rabbit holes de produto + +- **Redirecionar só `/campanha`.** Deixaria o resto da URL antiga morto sem destino — usuário com link antigo de artigo/capa cairia em erro. **Corte neste item:** path preservado para todas as rotas; `/campanha` é o caso obrigatório, o resto é grátis. +- **Redirecionar antes da migração.** Apressar o redirect devolveria o canônico sem os dados finais para quem vem da URL antiga. **Corte:** ordem travada — desativação (OPS80) → migração (OPS79) → redirect (este item). +- **SEO/canonical vira redesenho.** Melhorar ranking/`` do site público é outro esforço. **Corte neste item:** foco na transição de domínio; SEO é plano próprio. + +## Questões em aberto (produto) + +- **Redirecionar TODAS as rotas da URL antiga ou só `/campanha`?** **Opções:** (A) todas as rotas, preservando path; (B) só `/campanha`. **Recomendação:** (A) — `/campanha` é caso obrigatório; o resto é grátis e elimina qualquer URL morta. _(assumido — validar com produto)_ +- **Onde terminar o redirecionamento?** **Opções:** (A) no nível de hosting/DNS (fora do app); (B) dentro do app. **Recomendação:** (A) — o app continua 100% canônico e o cutoff não depende de deploy; decisão de onde exatamente é do executor. _(assumido — validar com produto)_ + +## Referências + +- Estado vivo (2026-08-23): `pt.jorgesolla.com.br` responde 200 via Vercel (antes da fase 1); `jorgesolla1313.com.br` responde 200 via Cloudflare (túnel do homeserver); sem host-redirect no app. +- Superfície que referencia a URL antiga no repo: `scripts/check-push-chain.mjs:13,47,49`; `tests/unit/campaignWebAuthnConfig.unit.spec.ts:17-52`; `tests/unit/campaignJsonMutationRoute.unit.spec.ts:27,54`; `tests/int/campaignMultiPhones.int.spec.ts:336`; `tests/unit/aiMarkdownLinks.unit.spec.ts:66`; `.opencode/agent/designer-campanha-solla.md:9,21`; `.opencode/agent/solla-comunicacao.md:19`. +- Docs históricos do cutover: `docs/plans/ops50*`, `ops51*`, `ops52-...-impl.md` (linha 104 — Vercel Blob pós-cutover), `CUTOVER-MAIN-ONLY.md`. +- `AGENTS.md` (Teqo context) — OPS50 removeu artefatos Vercel; `NEXT_PUBLIC_SITE_URL` obrigatória em prod via `getCampaignInviteBaseURL`.