From 2e389365a322ac54c78b7d18e8c9efc53865af66 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 15 Sep 2026 21:19:10 -0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20C169=20+=20C170=20+=20C171=20+=20?= =?UTF-8?q?OPS112=20=E2=80=94=20acervo=20(corte=20confi=C3=A1vel,=20sem=20?= =?UTF-8?q?teto,=20player)=20e=20modelos=20do=20worktree=20(Related=20#104?= =?UTF-8?q?5,=20#1046,=20#1047,=20#1048)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/c169-corte-vod-confiabilidade.md | 93 +++ .../plans/c170-corte-sem-limite-ui-draft.html | 586 ++++++++++++++++++ docs/plans/c170-corte-sem-limite.md | 106 ++++ ...71-player-youtube-sem-signin-ui-draft.html | 357 +++++++++++ docs/plans/c171-player-youtube-sem-signin.md | 102 +++ docs/plans/worktree-modelos-go-zen.md | 94 +++ 6 files changed, 1338 insertions(+) create mode 100644 docs/plans/c169-corte-vod-confiabilidade.md create mode 100644 docs/plans/c170-corte-sem-limite-ui-draft.html create mode 100644 docs/plans/c170-corte-sem-limite.md create mode 100644 docs/plans/c171-player-youtube-sem-signin-ui-draft.html create mode 100644 docs/plans/c171-player-youtube-sem-signin.md create mode 100644 docs/plans/worktree-modelos-go-zen.md diff --git a/docs/plans/c169-corte-vod-confiabilidade.md b/docs/plans/c169-corte-vod-confiabilidade.md new file mode 100644 index 00000000..ab29094d --- /dev/null +++ b/docs/plans/c169-corte-vod-confiabilidade.md @@ -0,0 +1,93 @@ +# Corte do acervo: resolver o trecho com a Câmara instável e falhar dizendo o porquê + +Status: rascunho +Atualizado em: 2026-09-15 +Issue: #1045 +Priority: P1 +Impeccable: A — N/A (sem UI nova: só o texto de uma falha existente muda) +Rascunho UI: N/A — sem UI nova +Appetite: ~1 dia eng; um outcome verificável — o "Cortar vídeo" não morre mais na primeira etapa quando o arquivo é recuperável, e quando não há arquivo a falha explica o que aconteceu e deixa tentar de novo +Responsável: — + +## Intenção + +Em produção (acervo de falas, id 997), a assessoria clica em "Cortar vídeo" e o corte morre na PRIMEIRA etapa — "Localizando o trecho na Câmara" — com um aviso de que a API da Câmara não está entregando o vídeo; mas o trecho está lá, baixável pelo link VOD, e a Câmara costuma gerar/entregar em dezenas de segundos. A janela de espera atual é curta demais para o comportamento real da API, o link já gravado na fala é ignorado como candidato e a causa real fica só numa coluna interna — a pessoa vê um texto genérico e um beco. Este item torna a primeira etapa resiliente (API lenta/instável ou hash morto não é o fim, enquanto existir arquivo verificável) e a falha honesta (diz o que houve e permite tentar de novo sem duplicar o corte). + +## Persona e fluxo + +- **Persona / contexto:** assessoria de comunicação (`communicator`, `coordinator`, `candidate`) na mesa, prazo curto, já com a seleção [início,fim] feita no detalhe da fala (C166) e o diálogo de corte aberto. +- **Job principal:** cortar o trecho exato sem saber — nem precisar saber — o que é CDN, hash ou timeout; se não der, entender o motivo e poder tentar de novo. +- **Fluxo desejado:** "Cortar e publicar" → "Localizando o trecho na Câmara" com espera suficiente para a API real → se vier hash morto, o link VOD já gravado da fala é tentado e verificado → arquivo encontrado: corta → se realmente não houver arquivo recuperável: falha que nomeia a causa ("ainda gerando", "não existe mais", "não foi possível cortar", "não deu para guardar") + "Tentar novamente" (mesma fala, mesmo trecho, sem corte duplicado) + "Voltar ao acervo". +- **Anti-goals de produto:** espelhar/re-hospedar MP4 (S3), baixar do YouTube, regenerar o acervo em lote, segundo fluxo/editor de corte, novo `Consent`/collection, upload externo, expor o acervo fora de `/campanha`. + +## Objetivo e aceite + +- **API lenta/instável:** a primeira etapa espera de acordo com o comportamento medido da Câmara (~30 s no primeiro request; depois consultas curtas) dentro de um limite declarado — não morre no primeiro timeout com "a Câmara não está entregando". +- **Hash morto com link ainda vivo:** se a resolução sob demanda falhar mas o link VOD gravado na fala responder arquivo real, o corte conclui com esse arquivo; link morto/falso (403, timeout, `text/html`) nunca é usado. +- **Sem arquivo recuperável:** a falha é honesta — a causa real chega à pessoa em linguagem clara (e o operador segue vendo o detalhe interno); nada é publicado e o fluxo não fica preso: "Tentar novamente" reusa a mesma row, sem duplicar. +- **Fala inelegível:** quando a fala nunca teve vídeo para cortar, a mensagem diz isso de forma direta (não culpa a API da Câmara). +- **Guardrails:** `excerptTMs` verbatim (nunca arredondar/deslocar o trecho); crédito `Fonte: Câmara dos Deputados · CC BY 4.0`; gate do acervo (`canReadSpeechCatalog`; `advisor`/`leader` negados fail-closed); sem espelhar MP4, sem upload externo, sem novo `Consent`/collection/migration; URLs públicas do acervo intocadas. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — progresso e erro são feedback de ação, não dado de acervo. +- **Decisões desbloqueadas:** a assessoria decide tentar de novo ou buscar outra fonte sabendo a causa; a coordenação decide se um link morto recorrente justifica reprocessar a fala. +- **Forma:** _adiada ao plano de implementação_ — restrição de produto: nada de dashboard/métrica de falhas; a causa é uma mensagem, não um relatório. + +## Dados da decisão (literais) + +- **API (resolução sob demanda):** `GET https://www.camara.leg.br/evento-legislativo/{eventId}/video-sob-demanda?idAudio={audioId}&trecho={excerptTMs}` → `{ estado: 'GERANDO' | 'PRONTO' | 'INDISPONIVEL', video: { linkParaReproducao, linkParaDownload, duracao, titulo } }`. +- **Trecho:** `trecho` = `excerptTMs` do banco (epoch ms), verbatim; valor vizinho cai em outro orador — nunca alterar. +- **Cadência medida (2026-09-12/14):** o primeiro request de um trecho novo pode levar ~30 s e devolver `GERANDO`; consultas seguintes (~5 s) devolvem `PRONTO` em 1–2 polls; o importador usa 40 polls × 5 s com timeout de 60 s. O resolver do player/corte hoje faz 2 tentativas × 15 s **sem poll** — janela insuficiente para o primeiro request. +- **Links gravados na fala (`vodPlaybackUrl`/`vodDownloadUrl`):** cache — candidatos a tentar quando a API falhar, **sempre verificados** (resposta real, não `text/html`) antes de virar arquivo; nunca requisito para tentar a Câmara. +- **Mensagens da 1ª etapa hoje (todas → `failed`):** fala ausente — "A fala deste corte não está mais disponível."; sem coordenadas — "Esta fala não tem trecho de vídeo para resolver na Câmara."; `GERANDO` — "A Câmara ainda está gerando o vídeo deste trecho."; não `PRONTO`/hash morto — "A Câmara não entregou o arquivo deste trecho."; `PRONTO` sem URL jogável — "A Câmara não entregou um arquivo jogável deste trecho."; transporte no catch — mensagem crua (`HTTP 403 em `, `fetch failed`, timeout). +- **Fallback visível hoje (diálogo):** `SPEECH_CUT_GENERIC_ERROR_MESSAGE` = 'Não foi possível preparar o corte. Verifique seu acesso e tente novamente.' — o aceite substitui/complementa por um vocabulário de causas. +- **Discriminador interno:** a coluna `error` da row `speechCut` (admin) registra a causa exata; `toSpeechCutViewModel` hoje **não** a expõe. +- **Evidência dos quadrantes (C162, 2026-09-14, 997 falas):** 773 ambos · 151 só VOD · 29 só YouTube · 44 nenhum; o CDN apagou os 924 MP4s entre 04:35 e 08:05; `PRONTO` velho pode devolver hash morto (403/timeout) e não aceita forçar regeneração (id 996) → tratar como indisponível. +- **Crédito:** `Fonte: Câmara dos Deputados · CC BY 4.0`. +- **Acesso/guardrails:** corta quem lê o acervo (`canReadSpeechCatalog`); `advisor` e `leader` negados fail-closed; leader lockdown intacto. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/utilities/speech/speechVodResolver.ts` (janela, poll e verificação dos links), `src/utilities/speech/speechCutJob.ts` (1ª etapa, candidatos e mensagens), `src/lib/speechVod.ts` (contrato de coordenadas/cache), `src/components/campaign/speech/SpeechCutDialog.tsx` (texto da cena de falha), `src/lib/schemas/speechCut.ts` (literais de erro) e a ação/rota do corte (`src/app/(campaign)/campanha/actions/speech.ts`, `.../acervo/cortar/`). +- **Precedente a olhar:** `scripts/lib/camaraFetch.mjs` (`resolveVod`: 40×5 s, timeout 60 s; `probeLink` decide por content-type) — referência de política, **não importável** de `src/`; `docs/plans/acervo-vod-sob-demanda.md` (C162, janela curta deliberada para o clique do player) e `docs/plans/c167-cortar-trecho-publicar-impl.md` (D2/D3: background, retry por row, reaper). O resolver é compartilhado com o player C162 — ampliar a janela afeta também o clique "assistir/baixar". +- **Testes/risco:** `tests/int/speechCut.int.spec.ts` hoje só cobre o caminho feliz stubado (nada pina a janela real, o poll ou o fallback de link); `tests/e2e/campaignSpeechCut.e2e.spec.ts` e o manifest de e2e mapeiam `src/utilities/speech`/`src/components/campaign/speech`/comunicacao. Não criar caminho de resolução paralelo ao C162 (editar o dono). + +## Dependências + +- Entregues (duras, satisfeitas): C162 (#999, resolução sob demanda), C167 (#1014, corte [início,fim] + retry sem duplicar); C168 (#1015, biblioteca de cortes) é superfície irmã. +- Irmão sem dependência dura: C170 (teto de duração da seleção) — não muda resolução nem tratamento de falha. + +## Fora de escopo + +- Espelhar/re-hospedar MP4 (S3/Garage), transcodificar, baixar do YouTube. +- Fila assíncrona/worker/cron, regenerar os 924 em lote, botão "gerar novamente". +- Redesenhar a cena de falha (o texto muda; o layout não), tela de pendências ou dashboard de falhas. +- Migration/collection/`Consent` novos; tornar o link do banco requisito; escrever o link novo resolvido de volta na fala (cache pode ser atualizado depois, fora desta fatia). + +## Rabbit holes de produto + +- **Re-hospedar o MP4 para "nunca mais falhar".** Se alguém "só completar": storage, custo, retenção e direitos. **Corte neste item:** resolver sob demanda com o que existir (API ou link gravado verificado). +- **Sincronizar todo link novo no banco (cron/reconciliation).** Se alguém "só completar": novas escritas e dono de permissão. **Corte neste item:** o banco segue cache; sem escrita nova obrigatória. +- **Mudar o trecho por vizinhança quando o exato falha.** Se alguém "só completar": muda o orador (fato medido). **Corte neste item:** `excerptTMs` verbatim; falha honesta. +- **Virar fila de suporte ("caso o erro persista, fale com…").** Se alguém "só completar": tickets, SLA, tela de acompanhamento. **Corte neste item:** causa + tentar de novo + voltar ao acervo. + +## Questões em aberto (produto) + +- **Quanto esperar na 1ª etapa?** **Opções:** A) alinhar ao comportamento medido (primeiro request ~30 s, depois consultas curtas) com limite declarado | B) manter curto e falhar rápido. **Recomendação:** A — esperar menos do que o medido é a causa provável do defeito; o limite continua bounded para não travar o job. _(assumido — validar no gate)_ +- **Usar o link VOD gravado?** **Opções:** A) só como candidato verificado depois de a API não servir | B) não usar (só API). **Recomendação:** A — foi pedido explícito ("o trecho é baixável pelo link VOD"); verificar antes de cortar e nunca entregar arquivo morto. _(assumido)_ +- **Até onde vai a mensagem honesta?** **Opções:** A) só a 1ª etapa (fatia mínima) | B) toda falha do corte, incluindo cortar/guardar (débito deferido no C167). **Recomendação:** B mínima — a causa já é gravada na row; expor um vocabulário curto de causas cobre a 1ª etapa e o débito sem virar tela nova. _(assumido)_ +- **Persistir o link recém-resolvido na fala?** **Opções:** A) não agora | B) sim. **Recomendação:** A — escrita nova exigiria permissão ampliada; fica como dívida registrada se o link morto se repetir. _(assumido)_ + +## Referências + +- GitHub Issue: — (após `pnpm agent:register`) +- Rascunho UI (gate): N/A — sem UI nova +- Planos irmãos: [`acervo-vod-sob-demanda.md`](acervo-vod-sob-demanda.md) (C162/#999), [`c167-cortar-trecho-publicar.md`](c167-cortar-trecho-publicar.md) (C167/#1014) + [`c167-cortar-trecho-publicar-impl.md`](c167-cortar-trecho-publicar-impl.md), [`c168-biblioteca-cortes.md`](c168-biblioteca-cortes.md) (C168/#1015) +- Pesquisa (cadência medida do VOD): `docs/research/piloto-fonte-videos-camara.md:39-42` +- Arquivos-chave (pista, não contrato): `src/utilities/speech/speechVodResolver.ts`, `src/utilities/speech/speechCutJob.ts`, `src/lib/speechVod.ts`, `src/lib/schemas/speechCut.ts`, `src/components/campaign/speech/SpeechCutDialog.tsx`, `src/app/(campaign)/campanha/actions/speech.ts`, `scripts/lib/camaraFetch.mjs`, `tests/int/speechCut.int.spec.ts`, `tests/e2e/campaignSpeechCut.e2e.spec.ts` +- `AGENTS.md` — vertical Comunicação, gate do acervo e crédito CC BY. + +## Self-score (shaping) + +5/5 — (1) fatia = um outcome verificável (a 1ª etapa resolve com a Câmara instável/link morto verificado; sem arquivo, a falha é honesta e retomável); (2) appetite de ~1 dia comporta janela/poll, candidato verificado, vocabulário de causas e testes, sem schema; (3) persona, job e aceite legíveis sem jargão de stack; (4) direção no codebase é hipótese (áreas, precedente não importável e o alerta de que o resolver é compartilhado com o player); (5) zero decisão dura de engenharia (rota, signatures, persistência e extração de helper ficam para o plano de implementação). diff --git a/docs/plans/c170-corte-sem-limite-ui-draft.html b/docs/plans/c170-corte-sem-limite-ui-draft.html new file mode 100644 index 00000000..5809e7ea --- /dev/null +++ b/docs/plans/c170-corte-sem-limite-ui-draft.html @@ -0,0 +1,586 @@ + + + + + + + UI draft — C170: corte sem teto (5 s até o fim da fala) + + +
+

+ UI draft — Acervo: cortar sem teto — do mínimo de 5 s até o fim da fala (C170) +

+

+ A assessoria seleciona um trecho longo (discurso inteiro, fala de vários minutos) no mesmo + player do acervo: o teto de 3 min sai dos textos e do servidor, o mínimo de 5 s permanece, e + o corte publicado/baixado corresponde exatamente ao [início,fim] escolhido. +

+

+ Cinza = estrutura existente · + acento = CTA primário e o que muda neste item (copy e + valores sem teto) · barras cinza = conteúdo não especificado. Cenas estáticas: sem + interação, sem dados reais. +

+ + + + +
+

+ Cena 1 · desktop (~1280px) — seleção longa (12 min) na barra de trecho, sem travar em 3 + min +

+
+
+

← Voltar ao acervo

+

DISCURSO EM PLENÁRIO

+

+ 11/08/2026 · 14:32 · Discurso em plenário · 21min07s +

+ +
+
+
+
+ ▶ +
+

+ Vídeo da sessão no YouTube +

+
+ + +
+
+ Trecho selecionado + + 02:14 → + 14:14 · + 12min0s + +
+ +
+
+
+
+
+
+
+ 00:00 + 02:14 + 14:14 + 21:07 +
+

+ Clique nas frases: o limite encaixa no segmento (ímã). Arraste as alças para o + ajuste fino. + Mínimo 5s · sem limite — até o fim da fala + — a duração aparece enquanto seleciona. +

+
+ +
+ + + + + +
+ +

+ Transcrição (clique nas frases para marcar o trecho) +

+
+
+ 00:00 + … senhor Presidente, colegas deputados … +
+
+ 01:10 + … agradeço a presença de todos … +
+
+ 02:14 + … a saúde pública na Bahia vive um momento decisivo … +
+
+ 05:30 + … o financiamento tripartite precisa ser cumprido … +
+
+ 08:10 + … municípios do interior não conseguem manter o atendimento … +
+
+ 13:45 + … defendo um compromisso com quem mora longe das capitais … +
+
+ 14:38 + … muito obrigado, Presidente. … +
+
+ 16:20 + … encerro pedindo apoio ao requerimento … +
+
+
+ + +
+
+
+

+ O teto de 3 min sai dos textos da barra; o teto passa a ser o fim da fala (21:07). O + mínimo de 5 s permanece — e a duração do trecho (12min0s) é o feedback que já existe. +

+
+ + + + +
+

+ Cena 2 · diálogo "Cortar vídeo" — duração longa com a copy nova (sem faixa de 5–180 s) +

+
+
+
+

Cortar vídeo

+

+ O arquivo sai com o trecho exato — nada antes, nada depois. +

+
+ +
+
+ 02:14 → 14:14 + + 12min0s · mínimo 5 s · até o fim da fala + +
+

+ "… a saúde pública na Bahia vive um momento decisivo …" +

+
+ +
+ +
+ Saúde pública: financiamento e acesso no interior da Bahia +
+
+ +
+ +
+ No pronunciamento, o deputado cobra o financiamento tripartite da saúde e defende + mais recursos para o atendimento no interior baiano. +
+
+ +
+ Sugerido por IA — revise antes de publicar. Os dois campos são editáveis. +
+ +
+ + +
+

+ Nada é publicado antes de confirmar. No fim, o corte ganha o link + /corte/123 e o arquivo fica na biblioteca de cortes + (C168). +

+
+
+

+ Só a linha do trecho muda: "dentro da faixa de 5–180 s" vira "mínimo 5 s · até o fim da + fala". A mensagem de erro do servidor e o prompt da IA também perdem o 180 (fora da tela). +

+
+ + + + +
+

+ Cena 3 · variantes da seleção — trecho de 12 min (A), até o fim da fala (B) e fala inteira + (C) +

+
+
+

A · trecho longo comum

+

+ 02:14 → 14:14 · 12min0s +

+
+
+
+
+
+
+

+ Antes: a barra travava o fim em 3 min. Agora vai até onde precisa. +

+
+ +
+

B · até o fim da fala (fim = duração)

+

+ 18:32 → 21:07 · 2min35s +

+
+
+
+
+
+
+

+ O fim encosta no último segundo da fala; o rótulo diz "até o fim da fala". +

+
+ +
+

C · fala inteira

+

+ 00:00 → 21:07 · 21min07s +

+
+
+
+
+
+

+ Permitido: o teto é a duração da fala. O mínimo de 5 s não some — seleções curtas + seguem bloqueadas. +

+
+
+
+ + + + +
+

+ Cena 4 · mobile (~390px) — mesma seleção longa, ações empilhadas +

+
+
+ ← + Comunicação + +
+ +
+

Acervo · fala

+

DISCURSO EM PLENÁRIO

+

11/08/2026 · 21min07s

+ +
+
+ ▶ +
+
+ +
+
+ Trecho selecionado + 02:14 → 14:14 · 12min0s +
+
+
+
+
+
+
+
+ 00:00 + 02:14 + 14:14 + 21:07 +
+

+ Mínimo 5s · sem limite — até o fim da fala — a + duração aparece enquanto seleciona. +

+
+ + + + + + +

Transcrição

+
+
+ 02:14 + … a saúde pública na Bahia … +
+
+ 08:10 + … o atendimento no interior … +
+
+ 14:38 + … muito obrigado, Presidente. … +
+
+ +

+ Fonte: Câmara dos Deputados · CC BY 4.0 +

+
+
+
+ + + + +
+

+ Cena 5 · mobile (~390px) — diálogo de corte com a copy nova +

+
+
+

Cortar vídeo

+

+ O arquivo sai com o trecho exato — nada antes, nada depois. +

+ +
+
+ 02:14 → 14:14 + 12min0s · mínimo 5 s · até o fim da fala +
+
+ +
+ +
+ Saúde pública: financiamento e acesso no interior +
+
+ +
+ +
+ No pronunciamento, o deputado cobra o financiamento tripartite da saúde e defende + mais recursos para o interior baiano. +
+
+ +
+ Sugerido por IA — revise antes de publicar. Os dois campos são editáveis. +
+ + + +

+ Nada é publicado antes de confirmar. O corte longo segue o mesmo fluxo — espera com + progresso honesto (C167). +

+
+
+
+ + + + +
+

O que o gate valida aqui

+
    +
  • + 1. O rótulo novo da barra ("Mínimo 5s · sem limite — até o fim da fala") e a ausência de + qualquer menção a 3 min/180 s nos textos visíveis. +
  • +
  • + 2. A seleção longa (12 min) cabe na barra e o fim pode encostar no fim da fala (cenas 1, + 3B e 4). +
  • +
  • + 3. O diálogo de corte mantém a hierarquia atual, só trocando a faixa de duração pela + copy nova (cenas 2 e 5). +
  • +
  • + 4. O resto do detalhe (player, transcrição com ímã, compartilhar, baixar, crédito CC BY) + fica intacto — a fatia não redesenha nada. +
  • +
+
+
+ + diff --git a/docs/plans/c170-corte-sem-limite.md b/docs/plans/c170-corte-sem-limite.md new file mode 100644 index 00000000..444fe253 --- /dev/null +++ b/docs/plans/c170-corte-sem-limite.md @@ -0,0 +1,106 @@ +# Acervo: cortar sem teto — do mínimo de 5 s até o fim da fala + +Status: rascunho +Atualizado em: 2026-09-15 +Issue: #1046 +Priority: P2 +Impeccable: B — encaixe no player/detalhe do acervo +Rascunho UI: docs/plans/c170-corte-sem-limite-ui-draft.html +Appetite: ~0,5–1 dia eng; um outcome verificável — qualquer trecho de 5 s até o fim da fala corta, publica e baixa exatamente o intervalo escolhido +Responsável: — + +## Intenção + +O corte do acervo trava em 3 min (180 s). A assessoria precisa cortar trechos longos — um discurso inteiro, uma fala de vários minutos — e esbarra num teto que não é do material, é da ferramenta: acima de 3 min a barra de seleção não anda e o servidor recusa o corte, então o trecho longo sai por fora (baixar a sessão, editar em outra ferramenta). Esta fatia remove o teto de produto: o limite passa a ser a própria fala — do mínimo de 5 s até o fim — e o corte publicado/baixado corresponde exatamente ao trecho escolhido. Sem superfície nova, sem editor: é a mesma seleção do C166 e o mesmo corte do C167, sem teto. + +## Persona e fluxo + +- **Persona / contexto:** assessoria de comunicação (communicator; coordenação e candidatura também), na mesa ou em campo, montando peça com o discurso inteiro ou uma fala de vários minutos. +- **Job principal:** selecionar e cortar o trecho exato que a peça precisa, inclusive um trecho longo, sem teto artificial de duração. +- **Fluxo desejado:** abre a fala → "Selecionar trecho" → marca o início nas frases (ímã) e leva o fim até onde precisar — até o fim da fala; a barra não trava mais em 3 min e a duração aparece enquanto seleciona → "Compartilhar" (C166) ou "Cortar vídeo" (C167) → o corte publicado/baixado começa e termina nos marcos escolhidos, mesmo com 12 min ou a fala inteira. +- **Anti-goals de produto:** não é editor de vídeo nem timeline com zoom; não muda o mínimo de 5 s; não mexe no mecanismo de corte nem no resolver (item irmão de pipeline, C169) nem na publicação/kill switch (C167/C168); não cria presets de duração nem seleção multi-trecho; não muda o gate do acervo nem o crédito CC BY. + +### Esboço de fluxo (B/C/D) + +```text +[detalhe da fala] → "Selecionar trecho" → [início nas frases] + [fim até onde precisar: ≥5s, até o fim da fala] +→ duração visível (ex.: 12min) → [Compartilhar (C166) | Cortar vídeo (C167)] +→ corte publicado/baixado = [início,fim] exatos (sem teto de 3 min) +``` + +### Rascunho UI (B/C/D) + +- Rascunho UI (gate): `docs/plans/c170-corte-sem-limite-ui-draft.html` — cenas: detalhe com seleção longa (12 min) e o rótulo novo do limite; diálogo "Cortar vídeo" com a copy nova; variantes (até o fim da fala; fala inteira); mobile com seleção longa e diálogo. + +## Objetivo e aceite + +- Selecionar [início,fim] de 5 s até a duração total da fala: o fim alcança o último segundo da fala e o início, o primeiro; a barra não trava mais em 180 s. +- O corte publicado/baixado corresponde exatamente ao trecho escolhido — começa e termina nos marcos escolhidos, inclusive trechos longos (12 min, fala inteira). +- Robustez: um corte longo acima dos ~3 min atuais conclui — o arquivo toca e baixa; se o pipeline falhar por limite operacional (download do VOD, tempo do ffmpeg), falha honesta como hoje e nada é publicado (causa no item de pipeline, não vira teto de produto). +- A copy diz a verdade: limite visível = "Mínimo 5s · sem limite — até o fim da fala"; a duração aparece durante a seleção. +- Mínimo de 5 s mantido (decisão de produto: evita corte de 1 s sem uso). _(assumido — validar no gate)_ +- Guardrails: `excerptTMs`/seleção exata [início,fim] intocados; crédito "Fonte: Câmara dos Deputados · CC BY 4.0"; gate do acervo intocado (communicator/coordinator/candidate; advisor/leader negados fail-closed); kill switch/publicação do C167/C168 intocados; sem editor de vídeo; sem trocar início/fim depois de cortado. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — a superfície é a barra de seleção e o diálogo de corte; duração/`mm:ss` é feedback de ação, não dado do acervo. +- **Decisões desbloqueadas:** a assessoria decide cortar um trecho longo (discurso inteiro, fala de vários minutos) sem sair da ferramenta; a coordenação decide o que fica no ar (C168). +- **Forma:** _adiada ao plano de implementação_ — restrição: sem KPI/telemetria de corte nesta fatia. + +## Dados da decisão (literais) + +- Teto atual **removido**: `MAX_EXCERPT_SECONDS = 180` (`src/lib/speechExcerptSelection.ts:21`), aplicado em `normalizeExcerptRange` (:54) e `moveRangeEdge` (:125,134). +- Mínimo **mantido**: `MIN_EXCERPT_SECONDS = 5` (`src/lib/speechExcerptSelection.ts:44-53`). +- Limite efetivo: `durationSeconds` da fala — [início,fim] pode ir de 5 s até a duração total (fala inteira permitida). +- Copy nova (exata): + - Barra de seleção (`SpeechExcerptControls.tsx:31`): `Mínimo 5s · máximo 3min` → `Mínimo 5s · sem limite — até o fim da fala`; `aria-valuemin/max` (:114,119) passam a refletir 0–duração. + - Diálogo de corte (`SpeechCutDialog.tsx:242-244`): `dentro da faixa de 5–180 s` → `mínimo 5 s · até o fim da fala` (linha vira ` · mínimo 5 s · até o fim da fala`). + - Mensagem de erro (`src/lib/schemas/speechCut.ts:17`): `Selecione um trecho de 5 a 180 segundos.` → `Selecione um trecho de 5 segundos até o fim da fala.` + - Prompt da IA (`src/utilities/speech/speechCutMetadata.ts:42`): `Receberá tipo, data, resumo oficial e a transcrição automática (ASR) de um trecho de 5 a 180 segundos.` → `Receberá tipo, data, resumo oficial e a transcrição automática (ASR) de um trecho da fala com 5 segundos ou mais.` +- Servidor: `src/app/(campaign)/campanha/actions/speech.ts:133` (`resolveExcerptRange`, também usado na sugestão de IA em :338) — o guard de máximo passa a ser só a duração da fala; `src/lib/schemas/speechCut.ts:16-17` — zod já limita 0–86.400 (`excerptBoundarySeconds`), o teto real é a action. +- Testes que pinam 180 a atualizar: `tests/unit/speechExcerptSelection.unit.spec.ts:41-42,113,130`; `tests/unit/speechDetailPlayer.unit.spec.tsx:290,306,321,444`; `tests/int/speechCut.int.spec.ts:300` (regex `/5 a 180/`). +- Limites operacionais que um corte longo encosta (aceite de robustez, sem engenharia aqui): `SOURCE_DOWNLOAD_TIMEOUT_MS=180_000`, `MAX_SOURCE_BYTES=2GiB`, `FFMPEG_MAX_TIMEOUT_MS=300_000`, reaper `SPEECH_CUT_STALE_MS=15min` (`src/utilities/speech/speechCutJob.ts:37-44`); qualquer corte >~37,5 s já roda no teto de 5 min do ffmpeg. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/lib/speechExcerptSelection.ts` (geometria/limites), `src/components/campaign/speech/` (`SpeechExcerptControls`, `SpeechCutDialog`), `src/app/(campaign)/campanha/actions/speech.ts` + `src/lib/schemas/speechCut.ts` (validação/servidor), `src/utilities/speech/speechCutMetadata.ts` (prompt da IA). +- **Precedente a olhar:** C166 (`c166-compartilhar-trecho-link.md`) e C167 (`c167-cortar-trecho-publicar.md`) — a barra e a validação nasceram com o teto; os testes acima são o mapa da mudança. +- **Risco de acoplamento:** o teto está espalhado (geometria, aria, action, schema, prompt, testes) — remover só a UI deixaria o servidor recusando o corte longo; `campaignSpeechAcervo` (e2e) e as units de `speechDetailPlayer`/`speechExcerptSelection` cobrem o caminho. + +## Dependências + +- Nenhuma dura. Observação: C169 (defeito do resolver) toca o pipeline de corte — validar um corte longo real depende do pipeline funcionar; risco registrado, sem bloquear. + +## Fora de escopo + +- Mudar o mínimo de 5 s; editor/timeline/zoom/precisão de frame; segundo player/preview do corte. +- Mecanismo de corte e resolver VOD (C169); publicação, kill switch e biblioteca (C167/C168). +- Corte em partes/streaming/fila assíncrona; presets de duração; seleção multi-trecho; legenda no corte. +- Ampliar papéis com acesso ao acervo; `Consent`/collection novas. + +## Rabbit holes de produto + +- **"Sem teto = cortar 2 h de sessão de uma vez."** Se alguém "só completar": ffmpeg em partes, fila, storage fragmentado. **Corte neste item:** o teto é a duração da fala; robustez mínima para um corte longo real concluir; infra de mídia não muda. +- **"Já que destravou, tira o mínimo também."** Se alguém "só completar": cortes de 1 s, dezenas por fala, biblioteca poluída. **Corte neste item:** mínimo de 5 s permanece. +- **"Aproveitar e refazer a barra com zoom."** Se alguém "só completar": timeline, precisão de frame, preview. **Corte neste item:** barra atual + ímã de frase, segundos inteiros. +- **"Preview do corte longo antes de publicar."** Se alguém "só completar": render extra, segundo player. **Corte neste item:** o progresso honesto do C167 já basta. +- **"Aumentar timeouts para garantir o corte longo."** Se alguém "só completar": mexer no pipeline às cegas. **Corte neste item:** se o pipeline não aguentar, é defeito do pipeline (C169) — não teto de produto. + +## Questões em aberto (produto) + +- **O mínimo continua 5 s?** **Opções:** A) manter | B) reduzir junto com o teto. **Recomendação:** A — 5 s evita corte de 1 s sem uso e não é o que a assessoria pediu. _(assumido — validar no gate)_ +- **Texto do limite?** **Opções:** A) `Mínimo 5s · sem limite — até o fim da fala` | B) só `até o fim da fala`. **Recomendação:** A — "sem limite" mata a memória do teto de 3 min e "até o fim da fala" diz qual é o teto real. _(assumido)_ +- **Selecionar a fala inteira é permitido?** **Opções:** A) sim, [00:00, duração] | B) exigir deixar algo de fora. **Recomendação:** A — o teto é a duração; a peça decide o recorte. +- **Corte longo que falha por limite operacional?** **Opções:** A) tratar como defeito do pipeline (C169), com aceite de robustez aqui | B) reintroduzir teto técnico. **Recomendação:** A — teto de produto não se justifica por timeout; a falha honesta já existe (C167). + +## Referências + +- GitHub Issue: — (após `pnpm agent:register`) +- Rascunho UI (gate): `docs/plans/c170-corte-sem-limite-ui-draft.html` +- Planos irmãos: [`c166-compartilhar-trecho-link.md`](c166-compartilhar-trecho-link.md), [`c167-cortar-trecho-publicar.md`](c167-cortar-trecho-publicar.md) +- Arquivos-chave (pista, não contrato): `src/lib/speechExcerptSelection.ts`, `src/components/campaign/speech/SpeechExcerptControls.tsx`, `src/components/campaign/speech/SpeechCutDialog.tsx`, `src/app/(campaign)/campanha/actions/speech.ts`, `src/lib/schemas/speechCut.ts`, `src/utilities/speech/speechCutMetadata.ts`, `src/utilities/speech/speechCutJob.ts` +- `AGENTS.md` — vertical Comunicação, gate do acervo e crédito CC BY. + +## Self-score (shaping) + +5/5 — (1) fatia = um outcome verificável (selecionar de 5 s até o fim da fala e cortar exato) sem nova superfície; (2) appetite de ~0,5–1 dia para remover o teto nas pontas + robustez do corte longo, com os limites operacionais declarados; (3) persona, job e aceite em linguagem de produto, com a copy literal separada; (4) direção no codebase é hipótese (áreas, testes que pinam 180 e o risco C169); (5) zero decisão dura de engenharia — mecanismo, schema e infra ficam na implementação. diff --git a/docs/plans/c171-player-youtube-sem-signin-ui-draft.html b/docs/plans/c171-player-youtube-sem-signin-ui-draft.html new file mode 100644 index 00000000..eaa6bebc --- /dev/null +++ b/docs/plans/c171-player-youtube-sem-signin-ui-draft.html @@ -0,0 +1,357 @@ + + + + + + + UI draft — Acervo: player do YouTube sem beco de signin (C171) + + +
+

+ UI draft — Acervo: player do YouTube sem beco de signin (C171) +

+

+ A assessoria abre a fala com vídeo no YouTube e o player pede login; a outra janela já está + logada e nada muda. Este rascunho valida a saída: o vídeo toca quando o YouTube deixa e, + quando não deixa, a página oferece em um clique uma superfície que toca ("Assistir na + Câmara") ou "Abrir no YouTube" no ponto exato. +

+

+ Cinza = estrutura existente · âmbar = novo neste item · + barras cinza = conteúdo não especificado. Cenas estáticas: sem interação, sem dados reais. +

+ + + + +
+

+ Cena 1 · desktop (~1280px) — o estado que a assessoria vê hoje + superfície alternativa +

+
+
+

← Voltar ao acervo

+

+ Título da fala — tipo de sessão +

+

+ dd/mm/aaaa · hh:mm · Tipo de sessão · duração · Presidiu: [placeholder] +

+ +
+
+ +
+ + renderizado pelo YouTube (iframe) + +
+ ▶ +
+

+ Faça login para confirmar que você não é um robô +

+ +

+ Abre outra janela — a página do acervo continua no mesmo estado. +

+
+ + +
+

+ Se o vídeo não abrir aqui, assista por outro caminho: +

+
+ + +
+

+ O trecho da Câmara toca nesta página; o YouTube abre no ponto da fala. Sem + trecho da Câmara, fica só "Abrir no YouTube". +

+
+ + +
+ + + +
+ +

+ Transcrição (clique para posicionar) +

+
+
+ 00:00 +
+
+
+ 00:43 +
+
+
+ 02:10 +
+
+
+ 04:02 +
+
+
+
+ + +
+
+
+
+ + + + +
+

+ Cena 2 · mobile (~390px) — embed bloqueado + saídas empilhadas +

+
+
+

← Voltar ao acervo

+

Título da fala

+

dd/mm/aaaa · Tipo de sessão · duração

+ +
+ + renderizado pelo YouTube + +
+ ▶ +
+

+ Faça login para confirmar que você não é um robô +

+ +
+ +
+

+ Se o vídeo não abrir aqui, assista por outro caminho: +

+
+ + +
+
+ +
+ + +
+ +

+ Transcrição (clique para posicionar) +

+
+
+ 00:43 +
+
+
+ 02:10 +
+
+
+ +

Fonte: Câmara dos Deputados · CC BY 4.0

+
+
+
+ + + + +
+

+ Cena 3 · desktop (~1280px) — YouTube tocando: as saídas continuam visíveis (discretas) +

+
+
+

← Voltar ao acervo

+

+ Título da fala — tipo de sessão +

+

+ dd/mm/aaaa · hh:mm · Tipo de sessão · duração · Presidiu: [placeholder] +

+ +
+
+
+ ▶ +
+ + Vídeo da sessão no YouTube — tocando + +
+ +
+ Se o vídeo não abrir aqui: + + +
+

+ O app não vê o erro interno do iframe: a saída fica ao alcance sem depender de + detectar o bloqueio. A hierarquia (quem fica âmbar) se decide no gate. +

+ +
+ + + +
+ +

+ Transcrição (clique para posicionar) +

+
+
+ 00:00 +
+
+
+ 00:43 +
+
+
+
+
+
+
+
+ + diff --git a/docs/plans/c171-player-youtube-sem-signin.md b/docs/plans/c171-player-youtube-sem-signin.md new file mode 100644 index 00000000..b76d3ff3 --- /dev/null +++ b/docs/plans/c171-player-youtube-sem-signin.md @@ -0,0 +1,102 @@ +# Acervo: o player do YouTube nunca termina num beco de signin — toca ou oferece a saída em um clique + +Status: rascunho +Atualizado em: 2026-09-15 +Issue: #1047 +Priority: P1 +Impeccable: B — encaixe no player do detalhe do acervo +Rascunho UI: docs/plans/c171-player-youtube-sem-signin-ui-draft.html +Appetite: ~0,5–1 dia eng; um outcome verificável — abrir uma fala com YouTube toca na página ou dá saída em um clique, nunca um beco de login +Responsável: — + +## Intenção + +A assessoria abre uma fala do acervo com vídeo no YouTube e o player pede "faça signin para exibir o vídeo"; clicar em signin abre outra janela (onde a pessoa já está logada) e nada muda na página — o pedido de login continua. Quem monta peça com prazo curto fica sem assistir a fala na página. Este item garante a saída: o vídeo toca embutido quando o YouTube deixa e, quando ele não deixa, a página nunca termina no beco de signin — oferece em um clique uma superfície que toca (o trecho da Câmara sob demanda, quando elegível) e/ou "Abrir no YouTube" no ponto exato. O erro interno do embed é invisível ao app, então a saída não pode depender de detectar o bloqueio: ela fica sempre ao alcance de um clique. + +## Persona e fluxo + +- **Persona / contexto:** assessoria de comunicação (communicator, coordenação e candidatura) em `/campanha/comunicacao`, na mesa ou em campo, produzindo peça. +- **Job principal:** assistir a fala na própria página, sem depender de login no YouTube e sem ficar presa num aviso de signin. +- **Fluxo desejado:** abre a fala com YouTube → o vídeo toca embutido → se o YouTube não liberar (login/idade/embed desabilitado), a pessoa vê que tem saída e clica "Assistir na Câmara" (toca o trecho exato nesta página) ou "Abrir no YouTube" (abre no ponto, na conta dela) → transcrição e clique para posicionar seguem funcionando. +- **Anti-goals de produto:** segundo player/editor; espelhar/baixar/transcodificar vídeo; autenticar no YouTube; detectar/inferir a causa do bloqueio; novo `Consent`/collection/migration; expor o acervo fora de `/campanha`; mexer nas URLs públicas. + +### Esboço de fluxo (B) + +```text +[abre fala com YouTube] → embed tenta tocar +→ YouTube libera → toca; saídas em 1 clique seguem visíveis ("Assistir na Câmara" | "Abrir no YouTube") +→ YouTube bloqueia (signin/idade/embed) → mesmas saídas logo abaixo do aviso +→ nunca um beco: sempre há um caminho que toca +``` + +### Rascunho UI (B) + +- Rascunho UI (gate): `docs/plans/c171-player-youtube-sem-signin-ui-draft.html` — cenas desktop (~1280px) e mobile (~390px) com o embed bloqueado + superfície alternativa; desktop com o YouTube tocando e as saídas visíveis. + +## Objetivo e aceite + +- Abrir uma fala com YouTube: se o YouTube permitir, o vídeo toca na página sem exigir login nosso e sem CTA de signin nossa. +- Se o YouTube não liberar o embed, a página NUNCA termina num beco: há sempre, em um clique, uma superfície que toca — "Assistir na Câmara" (trecho exato, quando a fala tem VOD) e/ou "Abrir no YouTube" (link `watch?v=&t=`, abre no ponto, na conta de quem já está logado). +- A saída fica ao alcance também no caminho feliz (o app não vê o erro interno do iframe): não é preciso saber que o YouTube bloqueou para se salvar. +- Hoje, com YouTube presente, o caminho da Câmara é inalcançável (o iframe ocupa o lugar e a CTA "Assistir o trecho" nunca aparece); a alternativa passa a conviver com o embed. +- Transcrição (clique para posicionar) e seleção de trecho (C166/C167) intactos; quando a superfície ativa for a da Câmara, o clique na transcrição posiciona nela. +- **Guardrails:** gate do acervo (communicator/coordinator/candidate; advisor/leader negados); crédito "Fonte: Câmara dos Deputados · CC BY 4.0"; `excerptTMs` verbatim (nunca aproxima); sem segundo player/editor; sem `Consent`/collection/migration; URLs públicas intocadas; a postura LGPD do embed na área interna é decisão de produto (questão em aberto). + +## Dados (intenção) + +- **Vou apresentar dados?** Não — a superfície é player + ações; nenhum KPI/gráfico novo. +- **Decisões desbloqueadas:** a assessoria decide assistir a fala na página sem depender do login do YouTube; a coordenação decide, pelo uso, se a postura `youtube-nocookie` na área interna vale manter. +- **Forma:** _adiada ao plano de implementação_ — restrição: o estado de bloqueio é feedback de ação (o que fazer agora), não dado de acervo. + +## Dados da decisão (literais) + +- **Embed atual:** `https://www.youtube-nocookie.com/embed/?playsinline=1&rel=0&start=` (params `playsinline=1`, `rel=0`; `start` só quando >0) — `src/components/campaign/speech/SpeechDetailPlayer.tsx:29-35`; iframe com `title="Vídeo da sessão no YouTube"`, `allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; compute-pressure"`, `allowFullScreen`, `loading="lazy"` (`:250-257`); sem `onError`, sem `enablejsapi`, sem fallback. +- **Se a recomendação A/C valer (trocar domínio):** `https://www.youtube.com/embed/?playsinline=1&rel=0&start=` — mesmos params, host sem `-nocookie`. +- **Link de "Abrir no YouTube" (já existe):** `https://www.youtube.com/watch?v=&t=` (`t=` omitido quando o offset é desconhecido) — `src/lib/speechShare.ts:29-35`. +- **Postura LGPD atual (site público, referência):** `https://www.youtube-nocookie.com/embed/i_fbclWWC5o?playsinline=1&rel=0` com comentário "youtube-nocookie keeps the embed LGPD-friendly" — `src/components/CampaignStorySection.tsx:9,54-61`. +- **Evidência das hipóteses:** o app não vê o erro do player (sem `onError`/API) e nenhum spec carrega o embed real — e2e aborta `https://www.youtube-nocookie.com/**` globalmente (`tests/e2e/fixtures/e2eTest.ts:108`); pins do domínio: `tests/unit/speechDetailPlayer.unit.spec.tsx:183,201,293`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts:163`, `tests/e2e/frontend.e2e.spec.ts:513`. Hipóteses do defeito (nenhuma confirmada): (a) bot-check do YouTube a embeds anônimos do nocookie ("Sign in to confirm you're not a bot"); (b) cookies de terceiros bloqueados — a sessão logada não chega ao iframe (explica a outra janela que "não muda nada"); (c) vídeo restrito por idade ou embed desabilitado; (d) restrição de embed de live archive da Câmara. +- **Copy pt-BR das saídas:** "Assistir na Câmara" (resolve e toca o trecho exato nesta página); "Abrir no YouTube" (abre no ponto); aviso: "Se o vídeo não abrir aqui, assista por outro caminho:" e apoio "O trecho da Câmara toca nesta página; o YouTube abre no ponto da fala." Sem fala não elegível ao VOD, fica só "Abrir no YouTube". +- **Hoje (inalcançável com YouTube):** `renderMedia` devolve o iframe primeiro e a CTA "Assistir o trecho" nunca aparece; sobra "Baixar vídeo (MP4)", que baixa mas não toca na página (`:248-258`, `:319-331`, `:405-429`). + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/components/campaign/speech/SpeechDetailPlayer.tsx` (embed + bloco de ações); `src/lib/speechShare.ts` (link de share); `src/utilities/speech/speechVodResolver.ts` + rota `acervo/resolver-vod` (reuso do resolver C162 — nenhum segundo player). +- **Precedente a olhar:** `src/components/CampaignStorySection.tsx` (embed nocookie do site público); estados honestos do próprio player (`StatusPanel`, "Não foi possível carregar o vídeo deste trecho."). +- **Risco de acoplamento:** C162/C166/C167/C169 tocam o mesmo arquivo — encaixe cirúrgico, sem reescrever quadrantes; gate do acervo e crédito CC BY valem; não há CSP/middleware/`headers()` no repo (a postura LGPD é convenção, não configuração). + +## Dependências + +- Suave: C169 (defeito do resolver no corte) — a superfície "Assistir na Câmara" fica mais confiável com o resolver corrigido; sem dependência dura. +- Entregues: C162 (player VOD sob demanda + YouTube default), C154/C166/C167 (detalhe, seleção, corte). + +## Fora de escopo + +- Fazer o YouTube tocar quando ele mesmo bloqueia o embed (login/idade/região) e autenticar/SSO no YouTube. +- Detectar ou classificar o motivo do bloqueio (o app não vê o erro interno do iframe). +- Baixar/espelhar/transcodificar vídeo; re-hospedar no S3 (C162/C167); redesenhar o detalhe/lista além do encaixe do player. +- Exposição pública do acervo; segundo player/editor; novo `Consent`/collection/migration. + +## Rabbit holes de produto + +- **"Fazer o YouTube sempre tocar".** Se alguém "só completar": OAuth, player próprio, proxy de mídia. **Corte neste item:** se o YouTube bloquear, a página oferece a alternativa em um clique. +- **Detectar o bloqueio no iframe.** Se alguém "só completar": IFrame API + eventos + polling + estados condicionais. **Corte neste item:** a saída é independente de detecção — sempre visível; sem prometer aviso automático. +- **"Login resolve".** Se alguém "só completar": round-trip de sessão para dentro do iframe (impossível com cookies de terceiros). **Corte neste item:** o embed é anônimo; o login é problema do YouTube, a saída é nossa. +- **Reescrever o player inteiro.** Se alguém "só completar": novos quadrantes, novo layout, tocar C162. **Corte neste item:** encaixe cirúrgico conforme rascunho; comportamento existente (transcrição, seleção, download) intacto. + +## Questões em aberto (produto) + +- **Trocar o domínio do embed de `youtube-nocookie.com` para `youtube.com`?** **Opções:** A) sim (mesmos params, só o host) — ataca as hipóteses (a)/(b) de bot-check/sessão e é o caminho direto para "toca sem login" | B) manter `nocookie` e apostar só nas saídas | C) ambos (trocar o host E manter as saídas). **Recomendação:** C — a troca é o remédio barato para embeds anônimos recusados e as saídas cobrem o que o YouTube bloquear de fato (idade, embed desabilitado); a área é interna e autenticada, então o custo LGPD é menor que no site público, mas como mexe na postura do repo, validar no gate. _(assumido — validar com produto)_ +- **As saídas ficam sempre visíveis ou só quando o YouTube falha?** **Opções:** A) sempre (secundárias, junto das ações do detalhe) | B) só no estado de bloqueio (exige detecção). **Recomendação:** A — o app não vê o erro interno do iframe; esperar a detecção é o que produz o beco de hoje. _(assumido)_ +- **"Abrir no YouTube" abre na aba nova ou embutido?** **Opções:** A) aba nova com o link de share no ponto | B) trocar o embed para `youtube.com` na mesma página. **Recomendação:** A como secundária (link já validado do C166); B é efeito da troca de host, não um botão. _(assumido)_ +- **O que o aviso diz?** **Opções:** A) explica e oferece as saídas ("Se o vídeo não abrir aqui, assista por outro caminho:") | B) só oferece as saídas, sem explicar. **Recomendação:** A, sem culpar quem usa. _(assumido)_ + +## Referências + +- GitHub Issue: — (após `pnpm agent:register`) +- Rascunho UI (gate): `docs/plans/c171-player-youtube-sem-signin-ui-draft.html` +- Planos irmãos: `acervo-vod-sob-demanda.md` (C162/#999), `c166-compartilhar-trecho-link.md`, `c167-cortar-trecho-publicar.md`, `c168-biblioteca-cortes.md` +- Arquivos-chave (pista, não contrato): `src/components/campaign/speech/SpeechDetailPlayer.tsx`, `src/lib/speechShare.ts`, `src/utilities/speech/speechVodResolver.ts`, `tests/unit/speechDetailPlayer.unit.spec.tsx`, `tests/e2e/campaignSpeechAcervo.e2e.spec.ts`, `tests/e2e/fixtures/e2eTest.ts`; `AGENTS.md` / `AGENTS-campaign.md` — vertical Comunicação, gate do acervo e crédito CC BY. + +## Self-score (shaping) + +5/5 — (1) fatia = um outcome verificável (tocar na página ou sair em um clique, nunca beco de signin); (2) appetite de ~0,5–1 dia comporta embed + saídas + pins de teste, sem schema; (3) persona, job e aceite legíveis sem jargão de stack; (4) direção no codebase é hipótese (áreas, precedente e pins como pista); (5) zero decisão dura de engenharia — host do embed, props e layout ficam como questão de produto/implementação. diff --git a/docs/plans/worktree-modelos-go-zen.md b/docs/plans/worktree-modelos-go-zen.md new file mode 100644 index 00000000..7b49243e --- /dev/null +++ b/docs/plans/worktree-modelos-go-zen.md @@ -0,0 +1,94 @@ +# Modelos padrão do worktree: `--go` → DeepSeek V4.1 Flash e `--zen` → Muse Spark 1.3 Free + +Status: rascunho +Atualizado em: 2026-09-15 +Issue: #1048 +Priority: P2 +Impeccable: A — N/A sem UI +Rascunho UI: N/A — sem UI +Appetite: ~0,5 dia eng — 2 valores do mapa + textos + pin +Responsável: — + +## Intenção + +Com o lançamento do DeepSeek V4.1 Flash (2026-09-10), ele virou também o modelo mais vantajoso no OpenCode Go — hoje `pnpm worktree … --go` abre a sessão em `opencode-go/hy3`, e o humano quer a flag apontando para o V4.1 Flash. Na mesma passada, `--zen` deve abrir no "Muse Spark 1.3 Free" em vez do atual `opencode-go/ox-alpha-free`. São dois valores do mapa fixo de flags: nada de mecanismo, cardápio ou preset. + +## Persona e fluxo + +- **Persona / contexto:** mantenedor/agente abrindo worktrees de Issue no terminal, escolhendo o modelo com uma flag no momento da invocação. +- **Job principal:** abrir a sessão já no modelo desejado com um único flag, sem editar config nem decorar ID. +- **Fluxo desejado:** `pnpm worktree next|plan|new|fix --go` (ou `--zen`) → claim/provisionamento como hoje → diretiva `launch … --model ` → sessão nasce no modelo certo; footer confirma modelo/provider. +- **Anti-goals de produto:** não vira seletor de modelos nem flag nova; não muda o preset sem flag; não mexe nas outras flags; não persiste preferência. + +## Objetivo e aceite + +- `pnpm worktree next|plan|new|fix --go` abre a sessão em `opencode-go/deepseek-v4.1-flash`; `--zen` abre em `opencode/muse-spark-1.3-contributor-free`. +- Superfícies que citam o mapa saem sincronizadas na mesma entrega (código, docblocks/help, shell, command, skill, pin unitário) — sem contrato divergente. +- Sem flag, o preset `deepseek/deepseek-flash` permanece byte a byte; `cheap/pro/alibaba/glm/free` e o fail-high at-most-one intactos. +- Guardrail: nenhuma menção a `--variant` volta à diretiva; config global da máquina fora do repo. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — tooling de dev, sem métrica de produto. +- **Decisões desbloqueadas:** dev/agente confia que `--go`/`--zen` apontam para os modelos correntes, sem consultar o catálogo antes de cada invocação. +- **Forma:** N/A — item config-only. + +## Dados da decisão (literais) + +| Flag | Valor hoje | Valor novo | +| ------- | --------------------------- | ------------------------------------------ | +| `--go` | `opencode-go/hy3` | `opencode-go/deepseek-v4.1-flash` | +| `--zen` | `opencode-go/ox-alpha-free` | `opencode/muse-spark-1.3-contributor-free` | + +- Preset sem flag (intocado): `deepseek/deepseek-flash` (`OPENCODE_PRESET_MODEL`; override `OPENCODE_WORKTREE_MODEL`). +- IDs verificados no catálogo local (`opencode models`, 2026-09-15): `opencode-go` tem `deepseek-v4.1-flash` ("DeepSeek V4.1 Flash"); o `-free` do Muse Spark só existe no provider `opencode` (OpenCode Zen) — `opencode-go` só tem `muse-spark-1.3-contributor` (sem free); logo `--zen` passa a apontar para o provider `opencode`, coerente com o nome da flag. +- Chaves intocadas do mapa: `cheap=cheapestinference/deepseek-v4-flash`, `pro=deepseek/deepseek-v4-pro`, `alibaba=alibaba-token-plan/deepseek-v4-flash`, `glm=opencode-go/glm-5.3-flash`, `free=openrouter/openrouter/free`. + +## Superfícies a sincronizar (caminho, não linha) + +- `scripts/lib/worktree.mjs` — `WORKTREE_MODEL_MAP` (dono do mapa) + docblock do mapa (nota OPS112). +- `scripts/worktree.mjs` — docblock (o help interpola o mapa e se atualiza sozinho). +- `.agents/shell/worktree.sh` — docblock da função (lista de IDs e nota histórica do `--go`). +- `.opencode/commands/worktree.md` — bloco do `next`. +- `.agents/skills/worktree-next-issue/SKILL.md` — prosa do default-go e do launch (manter a história OPS93/95 e ajustar os valores correntes). +- `tests/unit/worktree.unit.spec.ts` — pin `toEqual` do mapa. +- `.agents/skills/local-database/SKILL.md` — cita o mapa sem IDs: confirmar que nada muda. +- Nova entrada `docs/changelog/2026-09-15-ops112.md` (histórico existente não se edita). + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `scripts/lib/worktree.mjs` (fonte única), textos que o citam (acima) e o pin unitário. +- **Precedente a olhar:** OPS95 (`docs/plans/worktree-flags-modelo-correcao.md` — values-only em 2 chaves + sync + pin), OPS100 (`--glm`/`--free`), OPS101 (preset canônico). +- **Risco de acoplamento:** `scripts/worktree.mjs` está em `HIGH_RISK_EXACT` (`scripts/lib/test-affected-core.mjs`) → diff nele dispara unit/int full + e2e curado no PR; `scripts/lib/worktree.mjs` não é high-risk. Sem DB, migration, access, UI ou e2e novo. + +## Dependências + +- Nenhuma. + +## Fora de escopo + +- Preset sem flag (`deepseek/deepseek-flash`) e override `OPENCODE_WORKTREE_MODEL`. +- Demais flags do mapa (`cheap/pro/alibaba/glm/free`) e o mecanismo fail-high at-most-one. +- Reintroduzir `--variant`/variantes na diretiva (OPS95) ou mexer na config global da máquina. +- Editar docs históricos/congelados (`docs/plans/*` anteriores, changelogs, HISTORY). + +## Rabbit holes de produto + +- **Revisar o cardápio inteiro.** Se alguém "só completar": re-checar preço/capacidade e reescrever todas as chaves. **Corte neste item:** só `go` e `zen`. +- **Sincronização parcial.** Atualizar código e esquecer shell/command/skill/pin → contrato divergente (histórico OPS24/OPS95). **Corte:** todas as superfícies acima na mesma entrega. +- **Trocar o preset "de brinde".** O V4.1 Flash já é o preset; qualquer mudança nele fica fora. + +## Questões em aberto (produto) + +- Nenhuma — a única dúvida (o `--zen` apontar para o provider `opencode`/Zen, não `opencode-go`) foi resolvida na exploração: o ID do catálogo com o nome "Muse Spark 1.3 Free" é `opencode/muse-spark-1.3-contributor-free`, coerente com o nome da flag. + +## Referências + +- `docs/plans/worktree-flags-modelo-correcao.md` (OPS95 — precedente de correção values-only + sync) +- `docs/plans/worktree-modelo-canonico-deepseek-flash.md` (OPS101 — preset canônico; não mexer) +- `scripts/lib/worktree.mjs` (mapa/preset — fonte única), `tests/unit/worktree.unit.spec.ts` (pin) +- `scripts/lib/test-affected-core.mjs` (HIGH_RISK_EXACT de `scripts/worktree.mjs`) + +## Self-score (shaping ≥4) + +1. Um outcome verificável? **Sim** — flags resolvem para os IDs novos. 2. Appetite declarado e cabe? **Sim** — 2 valores + textos + pin. 3. Persona/job/aceite claros? **Sim**. 4. Direção é hipótese? **Sim** — caminhos sem contrato técnico. 5. Zero decisão dura de engenharia? **Sim** — valores literais são produto (IDs pedidos). **Total: 5/5.**