Classificação automática com proveniência (job + CLI recordings:classify), facetas ano/tema/alcance/município/duração na URL canônica, busca por tema no corpus próprio, ordenação e a barra de filtros portada do design aprovado sobre os shells compartilhados. Deep-links existentes byte-idênticos; Câmara intocada.
Collection contentEvent (hidden, sem hooks, sem PII) + beacon público
POST /api/content-events fail-soft, download contado no servidor na rota
de mídia, agregação em uma query group-by e a coluna "Circulação" na lista
interna + painel na ficha (port classe-a-classe do artefato estendido,
crítica final do designer certificada).
- snapshot drizzle da migration regenerado pós-rebase (inclui as tabelas do
S27; sem ele o próximo migrate:create recriaria content_piece)
- manifest e2e: CampaignFooter/jingleReads/campaignTime acordam o
frontendShareLink; dynamicParams inerte removido da rota .ics
- anúncio: dl válido (Local sem dd), aria-live só no status/chip, sem
hasImage/cast; hook direct deixa o erro de URL falar antes (+ teste int)
- débitos triados no impl plan (folding .ics -> #1281; deferidos com gatilho)
Related #1268
- shareLink ganha modo (direct|announcement), pool de destinos com "Destino no
ar" por linha e dados do evento; destination escalar sai via migration com
backfill lossless (legado vira direct com destino no ar)
- página de anúncio pública portada classe-a-classe, header campaign-site
extraído (CampaignPageHeader) e ativação/troca sem reload por poll da rota
/api/share-link/<slug>/live (fail-closed, no-store)
- agenda Google (action=TEMPLATE) + .ics por rota sob o slug; compartilhar o
link curto (kind event); S19 intacto e 404 uniforme preservado
Related #1268
/conteudos + /conteudos/<slug>: catálogo filtrável (Tipo · Cidade · Região ·
Tema · Instituição + termo na URL canônica), play sob demanda (Vídeo/Áudio só
carregam no toque; Foto com preview leve), Baixar e Compartilhar do próprio
card com a mensagem de voto por tipo (wa.me sem destinatário, editável; "A
mídia" usa a folha nativa e cai em download onde o aparelho não suportar) e o
card personalizado como peça sintética do catálogo → /cards.
- leitura pública nova (contentPieceReads) cacheada na tag contentPieces com o
gate where status=publicado + contentPieceIsPublic; a rota GET
/conteudos/<slug>/midia reusa buildPrivateMediaResponse (range, disposition,
private no-store, 404 uniforme) com download jorge-solla-1313-<slug>.<ext> e
a allowlist inline do dono estendida (webp/gif/avif + áudios das peças)
- contrato público puro do catálogo (filtros/VM/download) + mensagens de voto
(contentPieceShare); OG com fallback do global; conteudos entra nos slugs
reservados; kill switch por peça vale na hora (lista, página, rodapé/selo);
descoberta condicional no CampaignFooter e no header local
- UI portada classe-a-classe do design aprovado (cenas 01–08, com as cenas
06–08 estendidas no trigger a); sem migration, sem Consent e sem access novo;
contentMedia segue privada (sem segunda cópia de mídia)
- testes: 2 unit specs novos, 18 casos int (leitura pública + rota de mídia) e
e2e frontendConteudos (5 casos); manifest e curado atualizados
- follow-up de engenharia: #1276 (docs/plans/escala-dry-pos-s27.md)
Closes#1255
Envio em lote (1 request raw-body por arquivo, fan-out ≤2, até 50 arquivos) e
adicionar por link (Instagram/YouTube; extração só pela Graph API da conta
própria, senão peça-link) em /campanha/comunicacao/conteudos. Cada peça tem
estado próprio (Processando/Pronto/Falhou + Reprocessar), transcrição/texto e
catalogação automática revisável; publicar/despublicar preserva arquivo, slug
e publishedAt.
- collections contentPiece + contentMedia (upload privado no S3) e migration
20260923_032714_add_content_piece (slug único gerado no 1º publish,
sourceUrl única, searchText com índice trigram)
- pipeline reusa C199 (job after() + reaper + retry condicional, transcrição
DeepInfra, ffmpeg/chunk), o classificador de facetas do acervo e o
institutionCatalog; curatedFields impede o job de sobrescrever curadoria
- gate único canReadCommunicationCatalog (advisor/leader negados); delete
só pelo admin; sem Consent novo
- UI portada do design aprovado (lista desktop + cards mobile, diálogo de
lote com progresso por arquivo, diálogo de link, ficha editável, kill
switch); nav/chrome/column-picker/manifest atualizados
- instagramFeed expõe mediaUrl; revalidateTag ganha wrapper seguro fora de
request (fixtures/scripts)
- testes: 2 unit specs, 1 int spec (16 casos) e 7 e2e na vertical
- follow-up de engenharia: #1267 (docs/plans/escala-dry-pos-c211.md)
Closes#1254
O resolveShareLinkOgImageUrl absolutizava /api/media/file/… contra o siteUrl do global metadata (hoje https://jorgesolla.com.br, servido pelo WordPress antigo), que responde 404 no proxy de mídia — o WhatsApp lia título/descrição mas derrubava a miniatura (caso real do cartão plenaria-cultura, da plenária de 19/09).
- resolveDeploymentOrigin (novo em seo.ts): NEXT_PUBLIC_SITE_URL primeiro (inlinado no build de produção, mesmo carrier do isStagingSite), fallback no URL do global; canonical/og:url seguem a cadeia global-first documentada — só a URL do asset muda.
- Unit do resolver (env vence/fallback/vazio); int pina o env sobre um URL de global estrangeiro.
- player mobile 226px (CTA do detalhe acima da dobra), 330px a partir de lg
- painel de downloads no mobile: CTA primário primeiro, 'Outros arquivos' depois
- cards mobile só com título/funcionalidade; data e 'Abrir reel' a partir de lg
- despublicado: aviso + publicação antes da mídia bloqueada; 'Sobre o reel' depois
- feature sem '(#cards)' na UI e data de publicação só com o dia
- 'Abrir reel' aria-hidden (o card inteiro é o link)
Design tier: openai/gpt-5.6-sol — crítica final certificada (v3, 10 capturas).
Lista publicados-only com capas 9:16, detalhe com player 9:16 e painel de
downloads (vídeo sem áudio primário; com áudio, mp3 e .srt quando existem;
transcrição em texto) e kill switch reversível: despublicar tira da lista e
bloqueia a mídia (C193 decision B), republicar reativa e re-carimba publishedAt.
Fiação: gate communicationCatalog (rename do speechCatalog), 3º sub-item da
vertical no nav, rule da lista no chrome, paths e loader próprios; sem
migration, sem Consent, sem tocar Media.read ou a rota de mídia do C193.
Design tier: DEGRADED — crítica final do designer indisponível no tier
primário (limite de uso); requer sign-off humano antes de push/PR.
Modo "Por tema" no acervo de falas: expansão determinística do tema via
DeepSeek (generateObject, timeout, fallback null) alimenta o motor lexical
existente (searchText/keywords OR), mantendo facetas AND, paginação por fala,
o OR de cortes do C174 e o access control intacto. Resultado de tema mostra
selo e "Por que apareceu" com trecho real (frase contínua), sem score;
degradação honesta para termo exato com aviso e vazio honesto com saídas.
- URL: mode=tema (default omitido; canonicaliza sem q), seletor otimista
- ai/expandSpeechSearchTheme + lib/speechThemeTerms (sanitiza saída do modelo)
- view model com themeMatchTerm + espelho speechMatchesSearchTerm
- UI do card de tema (selos, proveniência, ações) e aviso degradado
- sem migration/Consent/exposição pública; gate de leitura inalterado
- testes unit/int/e2e; design hi-fi estendido e certificado pelo designer
Terceira variante da família de dossiês: recorte pela área canônica do
acervo (SPEECH_TOPICS), com o pipeline C186/C187 generalizado para a forma
subject via THEME_UNIT (generalização aditiva; C186/C187 byte-idênticos).
- resolveSpeechTopic no dono speechFacets (fail-closed, sem slug inventado)
- themeSnapshot + extract-theme-snapshot (THEME_REPORT_CONFIRM=1, read-only)
- build-dossie-solla-tema (pack medido, PDF A4 + .md + boletim 1 página)
- skill/comando/2 subagentes (researcher por era + redator) + specs
- cenas theme-only no descritor (acervo universo/amostra, abrangência,
lacunas, hierarquia de fontes, pills do boletim) certificadas pelo designer
Closes#1143
Generaliza aditivamente o pipeline C186 (default município; specs verdes) via o
descritor scripts/lib/dossieUnit.mjs (municipality|institution), extrai o emit
A4/fit-guard para scripts/lib/buildPdf.mjs e a receita read-only para
scripts/lib/readOnlyExtract.mjs.
- Catálogo fail-closed src/lib/institutionCatalog.ts + institutionNameAliases.ts
(seed UFBA/Correios/Enfermagem; escape one-off --slug/--name; dedup).
- Skill/command/subagente /dossie-solla-instituicao; eras A/B/C reusadas.
- Abrangência instituição × setor × rede nunca somada; era sem fonte vira página
de lacuna explícita; lista capada declara "e mais N" (regra C188 no dono).
- Boletim modelo 1×A4 com fase junto ao valor, sem fontes, sem CTA; herda apenas
fatos com fonte do dossiê (incl. falas do acervo read-only).
- Sem schema/migration/DB write; artefatos gitignored.
Lista + detalhe em /campanha/comunicacao/acervo/cortes: busca por título/descrição,\norigem da fala, player do MP4, edição de título/descrição e kill switch de\npublicar/despublicar no mesmo /corte/<id>. Gate speechCatalog fail-closed para\nadvisor/leader. Sem collection/migration nova.
Seleção do C166 ganha "Cortar vídeo": título/descrição por IA (editáveis, com fallback determinístico), job assíncrono com ffmpeg por re-encode exato e polling dos quatro passos, e página pública unlisted /corte/<id> (noindex, OG, crédito CC BY) servindo o mesmo MP4. Collection speechCut com access canReadSpeechCut (advisor/leader negados) e kill switch por status; ffmpeg entra só no runner da imagem com FFMPEG_PATH para teste.
O events.watch serializa o expiration (int64 epoch ms) como string JSON;
o client tipava number|null e o motor lancava RangeError: Invalid time
value no new Date(...).toISOString(), gravando pushChannelError sem
persistir o canal.
- parseGoogleWatchExpiration (borda do client) normaliza string|number
para number|null com teto do range do Date; malformado cai no fallback
de PUSH_CHANNEL_TTL_SECONDS, nunca lanca.
- pushChannelError ganha a causa do fetch (error.cause) preservada no
teto de 500 chars.
- Unit: stub de transporte modela /watch (string real) + matriz do
parser; int: caminho number->ISO, fallback null->now+TTL e causa.
Closes#1009
- puros: classify/parse da URL legada (dc_20b/dc_20, montaPdf), build do PDF
direto com #page, cache key por publicação, plano de reparo e fallback
- camaraFetch: resolveOfficialPdfUrl segue o redirect via .leg.br (fail-closed)
e probeLink generalizado (Range + contentType)
- import: normaliza officialTextUrl no write path (cache <out>/diario por
publicação; falha preserva PDF direto anterior, senão null)
- novo modo --repair-links [--dry-run] com guard CAMARA_IMPORT_CONFIRM fora de
alvo local, relatório de cobertura e idempotência
Papel communicator com nav própria e gate speechCatalog; acervo em
/campanha/comunicacao/acervo com busca normalizada (destaque no trecho),
filtros de ano/tema/alcance/fase/município/duração em URL canônica e
paginação por discurso via speech.searchText (GIN trgm + backfill).
Detalhe com player nativo no trecho, transcrição clicável, baixar/abrir
fonte e fallback sem VOD. Resolve o débito do C153 (Sollinha fora da
vertical) e corrige o parser de keywords oficiais no dono.
Closes#956
O verify do deploy manual (run 34763253924, main 7ca708a3) falhou em
tests/int/googleCalendarSync.int.spec.ts ("a cancelled event...") nas duas
tentativas do retry: o spec isolava o espelho e a limpeza com
`{ title: { like: 'C114%' } }`, mas no Payload 3.82 `like`/`contains` compilam
para ILIKE '%…%' (contains, sem ancoragem) — um UUID de fixture de outro spec
embutindo o hex `c114` entrava no escopo e podia ser deletado pela limpeza.
Escopa passes e cleanup pelos ids criados (ownedActivityIds), com
`{ track: false }` para linhas deliberadamente alheias; teste de regressão do
cenário exato (falha sem o fix); pin de convenção em codebaseConventions
banindo marcador de título hex em tests/int; post-mortem e changelog.
Collections speech/speechSegment no grupo Comunicação, papel communicator
(leitura restrita) e índice trigram normalizado para a busca por segmento.
Import idempotente da Câmara (discurso→evento→trecho→VOD→ASR) com facetas
gazetteer+LLM, proveniência e preservação de curadoria manual; parser de
quem presidia; reusa a lógica pura do piloto C152 via scripts/lib.
Related #955