teqo-backup/docs/plans/backfill-acervo-falas-impl.md

38 KiB
Raw Permalink Blame History

Impl: Backfill 2011–2026 + import de produção do acervo de falas

Status: executado Atualizado em: 2026-09-14 Issue: #957 Intenção: docs/plans/backfill-acervo-falas.md Appetite restante: herdado (~1–2 dias + tempo de máquina)

Leitura da intenção

Outcome. As 54ª–57ª legislaturas processadas pelo pipeline do C153, gravadas no banco de produção (teqo_1313), com relatório honesto de cobertura por legislatura: total, com trecho, com vídeo, com segmentos, sem trecho, fallback e falhas — e links do VOD validados numa amostra. Idempotência comprovada (reexecução não recria, não re-transcreve). O valor é o histórico: "o Solla sempre defendeu isso" só existe com 2011–2023 no catálogo.

O que NÃO negociar.

  • Guardrail de escrita: só speech/speechSegment (+ rels de município); nenhum objeto de mídia no S3; crédito CC BY 4.0 mantido no runbook/changelog.
  • Idempotência por sourceKey (C153) — reexecutar é seguro e é o mecanismo de resume.
  • Relatório sem maquiar falha como sucesso: withoutExcerpt/failed aparecem por legislatura, não somem no total.
  • Plenário apenas; nada de UI; nenhuma migration/schema novo (se surgir necessidade, justificar no PR).
  • Produção é dado real: o caminho é o runbook, com guard explícito antes de qualquer escrita.

O que reavaliar.

  • "Publicar antes ou depois do C154": o C154 não está em main (changelog até 2026-09-13 só tem C153). A importação não depende do C154 (dados não quebram nada), mas a validação visual do acervo é do C154; se ele não estiver no deploy de main da Fase 3, isso fica registrado como pendência, não como bloqueio (o aceite deste item é dado + cobertura).
  • "54ª incluída": sim — 1 discurso, custo ~zero, e é exatamente o caso "fonte antiga sem trecho" que o relatório precisa mostrar (trechos por orador só existem de 2015 em diante, C152).
  • "Fallback YouTube para sessões sem trecho": o C153 já grava youtubeUrl do evento; "fallback" aqui é contabilizar esses casos na cobertura (fallbackYoutube), não transcrever outra fonte — isso continua fora de escopo.
  • Orçamento do D8: os ~777 são "novos além da prova local da 57ª"; em produção o banco parte vazio, então as ~1.011 passam pelo pipeline (a 57ª re-transcreve). O relatório mede e o custo declarado é teto, não promessa.

Abordagem recomendada

Evoluir o dono do pipeline (scripts/import-camara-speeches.mjs + scripts/lib/*) com três modos novos — --all, --coverage, --verify-links —, reusar os módulos existentes e operar no homeserver contra o banco de produção depois do deploy que aplica a migration do C153 e antes do merge do PR, com a prova viva anexada (precedente OPS79).

Reuso (depth check): withPayloadTransaction/upsertSpeechBundle (src/utilities/speech/speechImport.ts) para a escrita; camaraSpeeches.mjs (puro) e camaraFetch.mjs (HTTP/VOD/ASR) para o pipeline; assertLocalDatabase + isTruthyEnv/dieWithLabel/ensureCachedDownload de scripts/lib/cli.mjs; recover-media.mjs como molde de guard (MEDIA_RECOVER_CONFIRM), eco de alvo e artefato; ops79:migrate como molde de confirmação (OPS79_MIGRATE_CONFIRM) e de operação pré-merge; docs/ops/teqo-1313-deploy.md §OPS79/§OPS84 como molde de seção de runbook; drizzleResultRows + SQL agregado (precedente supporter/supporterListOverviewAggregate.ts) para a coverage.

flowchart TB
  subgraph F1["Fase 1 — código (branch C155)"]
    A["import-camara-speeches.mjs<br/>--all · --coverage · --verify-links<br/>CAMARA_IMPORT_CONFIRM"]
    B["camaraSpeeches.mjs puro<br/>aggregateBackfillRuns · selectLinkSample"]
    C["speechCoverage.ts<br/>SQL agregado read-only"]
    A --> B
    A --> C
  end
  subgraph F0["Fase 0 — pré-requisito de schema"]
    D["deploy manual de main<br/>verify full, staging, aprovacao<br/>aplica migrations C153"]
  end
  subgraph F3["Fase 3 — homeserver (tmux + tee)"]
    E["~/teqo-backfill no SHA do branch C155<br/>env teqo-1313 + proxy 5433"]
    F["camara:import --all<br/>54 a 57, ~1.011, 10 a 15h"]
    G["--coverage"]
    H["--verify-links 3"]
  end
  I[("producao teqo_1313<br/>speech · speech_segment")]
  K["vod.camara.leg.br / dadosabertos"]
  J["PR C155 (Closes #957)<br/>resumo + artefatos anexados"]
  D --> E --> F
  A -.-> E
  F -->|upsert transacional| I
  F --> G --> H
  G -->|read-only| I
  H -->|GET Range| K
  F -->|API eventos/VOD/ASR| K
  F --> J
  G --> J
  H --> J
Opções Recomendação Rejeitadas
A) Evoluir o dono: --all/--coverage/--verify-links no import do C153 + utility de coverage + docs A — um CLI, um artefato, caches em memória, agregação pura testável; knip e convenções de script continuam com um dono B) script novo de orquestração + script novo de coverage (segundo CLI/knip/drift, dois lugares para o mesmo bug); C) operar só com loop shell no runbook (sem relatório por legislatura como artefato, operador vira o agregador)

Decisões de engenharia

D1 — Modo --all no dono do pipeline (barato; a decisão que importa é não criar twin)

  • Opções: A) --all no import-camara-speeches.mjs, loop 54→57 num processo, relatório combinado com legislatures[] | B) script novo backfill-camara-speeches.mjs chamando o import por legislatura | C) loop shell no runbook concatenando relatórios à mão.
  • Recomendação: A — o dono do pipeline é o import; um processo preserva os caches eventsByDate/eventPages entre legislaturas, emite um JSON com totais combinados e bucket por legislatura, e mantém --legislature como retry direcionado. --all é mutuamente exclusivo com --date e com --legislature explícito; --limit/--skip-transcribe/--reclassify valem por legislatura. Checkpoint do JSON a cada legislatura (perda limitada a uma legislatura se a rede cair); falha dura em uma legislatura registra aborted e segue para a próxima, com exit 1 no fim.
  • Rejeitadas: B — segundo CLI para o mesmo pipeline = drift de flags, mais um entry em knip.json (exports:error) e duas cópias das mesmas regras; C — sem artefato por legislatura (aceite pede), sem totais combinados e sem caches.

D2 — Relatório = métricas do run + cobertura do DB (caro: contrato do artefato; nomes de campo são fill-in)

  • Opções: A) ao final do --all, anexar coverage lida do banco por legislatura + modo read-only --coverage (sem rede, sem escrita no banco) | B) só o relatório do run | C) script separado camara-coverage.mjs.
  • Recomendação: A — run report conta o que a execução fez; coverage conta o que o banco tem. Reexecução resumida/parcial torna o run report não representativo do estado, e o aceite pede o retrato do acervo ("quantos têm vídeo, quantos caíram no fallback, o que ficou de fora e por quê"). getSpeechCoverage(payload) em src/utilities/speech/speechCoverage.ts com uma query agregada (speech LEFT JOIN speech_segment, COUNT(*) FILTER) via drizzleResultRows; o --coverage reusa a mesma função e é a verificação pós-run em produção.
  • Rejeitadas: B — não distingue "pulou porque já tinha" de "não tem", não responde fallback; C — twin do script (mesmo problema do D1-B) e o modo read-only ficaria fora do dono.

D3 — Guard de escrita em produção (caro de reverter: semântica fail-closed)

  • Opções: A) CAMARA_IMPORT_CONFIRM=1 exigido em todo modo que escreve quando NODE_ENV === 'production' ou host do DB não-local ou ALLOW_REMOTE_DB setado; --coverage/--verify-links isentos (read-only); --skip-transcribe não isenta (escreve metadados) | B) flag CLI nova (--yes/--force) | C) confiar só no assertLocalDatabase + ALLOW_REMOTE_DB | D) exigir a flag sempre, inclusive local.
  • Recomendação: A — precedente literal de MEDIA_RECOVER_CONFIRM (scripts/recover-media.mjs:68,79-85) e OPS79_MIGRATE_CONFIRM (scripts/migrate-signature-orphan.mjs:46,54-60). O guard roda antes de getPayload (nenhuma conexão, nenhuma escrita), ecoa o alvo (host do DB, modo, range) como o recover-media, e o predicado puro requiresWriteConfirm({ nodeEnv, databaseUrl, allowRemoteDb }) mora em scripts/lib/cli.mjs (junto de LOCAL_HOSTS, onde a convenção manda) e é unit-testado. O assertLocalDatabase permanece como cinto para host remoto explícito, com o hint atualizado para apontar o runbook §C155.
  • Rejeitadas: B — flag por invocação não sobrevive a wrapper/alias e não descreve o alvo; o repo já tem o vocabulário de env flag; C — o furo nomeado: LOCAL_HOSTS inclui postgres (host do env file do homeserver) e o runbook usa o proxy socat em 127.0.0.1:5433 — um run acidental em produção passa silencioso; D — quebra o DX local (C153 usa pnpm camara:import sem cerimônia) sem ganho: o risco é o alvo remoto, não o comando.

D4 — Onde e quando o backfill roda (caro: escreve dado real; a ordem é irreversível sem rollback)

  • Opções: A) homeserver, checkout separado ~/teqo-backfill no SHA do branch C155, depois de um deploy manual de main (aplica a migration do C153) e antes do merge do PR, com relatório/prova no PR (precedente OPS79) | B) rodar local (teqo_wt155) e copiar tabelas para produção | C) rodar só local e adiar produção | D) rodar do ~/teqo-deploy.
  • Recomendação: A — produção está sem as tabelas speech* (migration para no batch 20260912_224719_add_activity_public_event) e sem a revisão C153; o deploy de main é pré-requisito de schema; o checkout separado não contamina o workspace do runner (que segue main e é gerenciado pelo deploy); operação antes do merge é o precedente OPS79 — o código do operador não precisa estar publicado para escrever dados. Sequência do runbook: deploy → git fetch/checkout do SHA → pnpm install → env → tmux.
  • Rejeitadas: B — ids de municipality/rels divergem entre teqo_wt155 e teqo_1313 (o upsert resolve slug→id por banco), cópia cross-DB artesanal de 3 tabelas sem transação e ASR pago duas vezes; C — falha o aceite "dados disponíveis em produção"; D — o workspace do runner é do deploy; misturar branch de feature contamina o próximo dispatch.
  • Opções: A) read-only --verify-links <n>: n discursos por legislatura (amostragem determinística por índice, estável por speechAt+id), playback e download, GET com Range: bytes=0-1023 e fallback HEAD em 405/501, timeout curto; não-200 = warning no artefato; 0 links verificados = exit 1 | B) curl manual no runbook | C) não validar | D) validar os 1.011×2 links.
  • Recomendação: A — reproduzível, vira artefato e cabe no aceite com n=3 (12 discursos, ~24 probes). Warning em vez de falha dura porque fonte antiga legitimamente não tem trecho e 404 isolado não invalida o acervo; o fail-closed em "0 verificados" evita verde vazio com rede caída. Implementa probeVodLink(url) em camaraFetch.mjs e selectLinkSample(rows, n) puro em camaraSpeeches.mjs.
  • Rejeitadas: B — não reproduzível e não agrega ao relatório; C — aceite pede explicitamente; D — 2.022 requests sem ganho sobre amostra por legislatura.

D6 — Artefato do relatório (barato; forma de guarda é fill-in)

  • Opções: A) JSONs em data/camara/reports (gitignored, .gitignore:77) + resumo no changelog + seção nova no runbook + anexo do JSON à Issue #957; nada commitado | B) commitar o JSON (ou recorte) no repo | C) só changelog | D) relatório em serviço externo.
  • Recomendação: A — data/camara é gitignored por contrato; o run completo tem speeches[] de ~1.011 entradas (grande e regenerável); o runbook é dono do como-fazer, o changelog do o-que-aconteceu. O resumo de coverage (pequeno) vai no corpo do PR; o JSON completo fica no homeserver (/srv/hdd/backups/teqo-camara/reports) e anexado à Issue (upload web). Arquivos: backfill-<runAt>.json, coverage-<runAt>.json, verify-links-<runAt>.json.
  • Rejeitadas: B — ruído de repo, format/knip e conflito garantido a cada reexecução; C — perde detalhe por legislatura/falhas; D — dependência externa sem necessidade.

D7 — Testes proporcionais (barato; o corte é fill-in)

  • Opções: A) unit dos puros (aggregateBackfillRuns, selectLinkSample, requiresWriteConfirm/isLocalDatabaseUrl) + int de speechCoverage com fixtures | B) e2e/CLI com rede | C) nenhum teste novo (o script não tinha) | D) extrair e testar parseArgs inteiro.
  • Recomendação: A — a aritmética do relatório e a amostragem são puras e falham em silêncio sem teste; o guard é segurança e vira predicado puro em scripts/lib/cli.mjs, unit-testado ao lado de cliEnvFlags.unit.spec.ts; a coverage é SQL agregado, provado por int com fixtures cobrindo legislaturas diferentes, com/sem trecho, com/sem segmento e fallback YouTube (tests/int/speechCoverage.int.spec.ts, padrão do speechImport.int.spec.ts). Somar aos 31 unit do módulo Câmara.
  • Rejeitadas: B — flaky, custo real de ASR e sem retorno sobre o unit; C — agregação pode somar errado sem ninguém ver; D — extrair a CLI inteira para testar parsing trivial é desproporcional (fill-in futuro se parseArgs crescer).

D8 — Forma da execução longa (barato; wall clock é física)

  • Opções: A) um processo --all sequencial, desatendido em tmux/nohup com tee, checkpoint por legislatura, reexecução idempotente como resume | B) paralelizar discursos | C) uma sessão por legislatura | D) chunk por --date/ano.
  • Recomendação: A — ~1.011 × ~46s ≈ 13h de parede (10–15h com overhead), sem rate-limit storm contra a Câmara/Deep Infra; caches em memória e um artefato. Reexecutar --all (ou --legislature N) pula ASR/LLM do que já está no banco (segmentCount > 0 e classifiedBy === 'llm'). Orçamento declarado: ASR ≈ US$0,45–0,60 (Whisper large-v3 a US$0,00045/min; C152 mediu US$0,0006–0,0021/discurso) + LLM ≈ US$0,07, medidos no relatório. A 57ª re-transcreve em produção (banco vazio; segmentos locais não são transferidos — D4-B).
  • Rejeitadas: B — concorrência sem necessidade, retry storms e relatório de falhas confuso; C — perde caches e agregação combinada, operador vira orquestrador; D — fragmenta o relatório sem mudar custo.

Componentes / mudanças

Arquivo Mudança
scripts/import-camara-speeches.mjs (dono) Flags --all (mutex com --date/--legislature explícito) e modos --coverage/--verify-links <n>; loop 54→57 com checkpoint JSON por legislatura; coverage anexada ao relatório final; guard CAMARA_IMPORT_CONFIRM (D3) antes de getPayload; eco do alvo; falha por legislatura não aborta o run inteiro; HELP atualizado; hint do assertLocalDatabase aponta o runbook §C155
scripts/lib/camaraSpeeches.mjs (puro) aggregateBackfillRuns(runs) (somas de totals/asr/llm/elapsedMs, por legislatura e combinado) e selectLinkSample(rows, n) (amostragem determinística)
scripts/lib/camaraFetch.mjs (HTTP) probeVodLink(url) — GET Range: bytes=0-1023, fallback HEAD, status/content-type/bytes, erro de rede = warning
scripts/lib/cli.mjs (CLI) isLocalDatabaseUrl(url) e requiresWriteConfirm({ nodeEnv, databaseUrl, allowRemoteDb }) puros; assert-local-database.mjs passa a consumir isLocalDatabaseUrl (refactor mínimo, semântica intacta)
src/utilities/speech/speechCoverage.ts (novo) getSpeechCoverage(payload) — query agregada por legislatura: total, withExcerpt (audio_id), withVideo (vod_playback_url), withSegments (speech_segment > 0), withoutExcerpt, withoutSegments, fallbackYoutube (audio_id nulo e youtube_url presente) + totais
tests/unit/camaraSpeeches.unit.spec.ts + unit de aggregateBackfillRuns/selectLinkSample
tests/unit/cliEnvFlags.unit.spec.ts + unit de requiresWriteConfirm/isLocalDatabaseUrl
tests/int/speechCoverage.int.spec.ts (novo) fixtures de speech/speechSegment cobrindo os buckets; cleanup no afterAll
docs/ops/teqo-1313-deploy.md Seção ## C155 — backfill do acervo de falas em produção: pré-requisito (deploy com migration C153), comandos, resultado e rollback
docs/changelog/2026-09-13-c155.md (novo) Entrada curta com os números medidos (preenchida na Fase 4)

Sem migration, sem schema, sem access, sem UI, sem package.json (o pnpm camara:import já existe). Nenhuma escrita fora de speech/speechSegment/speech_rels.

Dados → forma (relatório)

Coverage (por legislatura + totais):

Campo Definição Responde
total discursos da legislatura no banco tamanho do acervo
withExcerpt audio_id não nulo trecho identificado na página do evento
withVideo vod_playback_url não nulo tem vídeo (VOD)
withSegments ≥1 speechSegment tem transcrição minutada
withoutExcerpt audio_id nulo o que ficou de fora
withoutSegments 0 segmentos pendência de transcrição
fallbackYoutube audio_id nulo e youtube_url presente "quantos caíram no fallback"

--all (JSON): { runAt, mode: 'all', options, legislatures: [{ legislature, range, totals, asr, llm, elapsedMs, aborted, speeches[], failures[] }], totals/asr/llm/elapsedMs combinados, coverage }. O modo --legislature/--date mantém o shape do C153 e ganha coverage no fim (compatível com o artefato existente).

Impressão (stdout): bloco por legislatura (listados · processados (criados/atualizados) · com trecho/sem trecho · com segmentos · ASR min e US$ · LLM tokens e US$ · tempo · falhas), linha combinada, tabela de coverage e falhas listadas com speechAt/stage/mensagem.

--verify-links (JSON): { runAt, sample: { perLegislature, total }, checked: [{ legislature, speechId, kind: 'playback'|'download', url, status, ok, note }], ok, warnings, elapsedMs }. Não-200 entra em warnings; checked.length === 0 → exit 1.

Destino: resumo de coverage + verify-links no corpo do PR e no changelog; JSON completo do run anexado à Issue #957; nada em git; runbook registra os números e o rollback.

Fases verificáveis

Fase 0 — Pré-flight (0,5h, zero escrita). Confirmar no homeserver: revision do container teqo-1313; payload_migrations com 20260913_001112_add_speech_catalog e 20260913_001200_add_speech_segment_trgm_index; tabelas speech/speech_segment/speech_rels existindo e vazias. Checar C154 em main (registrar se ausente). Garantir DEEPINFRA_API_KEY (em ~/stack/.env ou adicionar a ~/stack/teqo-1313.env, chmod 600) — o modo de escrita sem --skip-transcribe passa a falhar cedo sem a chave. Definir --out /srv/hdd/teqo-backfill/camara (394GB; não encher /). Prova: saída dos dois checks de migration/tabela + chave presente (sem eco).

Fase 1 — Código (0,5–1 dia). Implementar D1–D3, D5, D7. Smoke local em teqo_wt155: pnpm camara:import --all --limit 2 (7 discursos, ASR real, ~6–8 min) → relatório com 4 buckets + coverage; reexecutar o mesmo comando → 0 criados, 0 ASR (idempotência viva); --coverage sem rede; --verify-links 1. Quota: sem migration; ~120 linhas somadas entre script/lib/utility; pnpm gate:fast verde; unit novos verdes; int de coverage verde.

Fase 2 — Docs (0,5h). Seção do runbook (pré-requisito, comandos, rollback) + changelog stub (números reais entram na Fase 4). Quota: ≤ ~80 linhas de runbook; pnpm format/lint verdes.

Fase 3 — Ops no homeserver (≈1h preparo + 10–15h máquina + ≈1h validação). 3.0 Deploy manual de main (workflow_dispatch: verify full → staging → aprovação → produção) — aplica a migration do C153. 3.1 ~/teqo-backfill: clone/fetch de github.com/fsolla/teqo.git, checkout do SHA do branch C155, pnpm install. 3.2 Env: set -a; source ~/stack/teqo-1313.env; set +a; export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}" (proxy teqo-1313-build-proxy); export CAMARA_IMPORT_CONFIRM=1; tmux + tee ~/c155-backfill-<data>.log. 3.3 Smoke de produção com idempotência viva: --all --limit 1 (4 discursos) e repetir → segunda passada 0 criados, 0 ASR; coverage = 4. 3.4 --all completo (checkpoint por legislatura; se abortar, retomar com --all ou --legislature N). 3.5 --coverage e --verify-links 3 (12 discursos, ~24 probes). 3.6 Coletar artefatos e o log. Prova: coverage total = 1.011 (54ª=1, 55ª=362, 56ª=414, 57ª=234); warnings de links documentados; log do tmux arquivado.

Fase 4 — Fechamento (≈1h). Atualizar changelog com números medidos; runbook com resultado + rollback; anexar JSONs à Issue #957; resumo no corpo do PR. Gates: pnpm gate:fast, pnpm knip, cycles, pnpm typecheck, test:unit, test:int; e2e esperado none (script/docs fora dos prefixos de risco — ci-scope decide; se cair em unmapped-risk, adicionar mapping). Push + PR Closes #957 + auto-merge. Quota: 1 PR; prova: check checks verde.

Ajustes descobertos na execução

  • Fase 3.0 cumprida por outro deploy. A C154 (3e062749) mergeou em main e o deploy manual de 2026-09-13 publicou 424ee311 em produção — o schema do C153 (speech/speech_segment + migrations 20260913_001112/001200) e o searchText da C154 já estão no teqo_1313. Nenhum deploy novo é pré-requisito do backfill; o workspace ~/teqo-deploy também já está no SHA novo.
  • Rede da workstation para a Câmara instável (hangs/HTTP 500 na API em 2026-09-13 à tarde; o homeserver responde 200 em ~2s). O smoke local da Fase 1 vira o smoke de produção da Fase 3.3 (mesmo código, rede confiável); unit/int locais continuam sendo o gate de lógica.
  • Paginação do import ganhou backoff (fetchJsonWithBackoff, 5 tentativas com espera exponencial até 30s) porque a API devolveu 500 na página 4 da 55ª durante o smoke — o comportamento de abortar só a legislatura e seguir continua, agora com mais paciência.
  • Cache/mídia do run de produção em --out /srv/hdd/backups/teqo-camara (/srv/hdd é root-owned; backups/ é do usuário — 392 GB livres).
  • Smoke de produção (2026-09-13, teqo_1313): --all --limit 1 processou 54ª/56ª/57ª (2 ASR, ~US$0,003) e abortou só a 55ª (HTTP 500 persistente na página 4 da listagem da API — não é o import). Reexecução do mesmo comando: 0 criados, 0 ASR, 0 LLM (idempotência viva em produção); coverage do banco bateu com o relatório. O guard CAMARA_IMPORT_CONFIRM=1 foi exercitado (sem a flag, o run morre antes de conectar).
  • Run completo em tmux no homeserver (c155-run.log/c155-run55.log + JSONs em /srv/hdd/backups/teqo-camara/reports/): 54ª/56ª/57ª em ~6,2h e 55ª em paralelo (~2,8h); retry por data das falhas (~2h) e consolidação read-only. Resultado: 997 discursos (54ª=1, 55ª=348, 56ª=414, 57ª=234) — 947 com trecho, 924 com vídeo, 920 com segmentos; ASR 925 chamadas / 45,4h / ~US$1,23; LLM ~US$0,11.
  • Fallback de listagem desceu dois níveis (dateRangeChunks ano→mês): a API responde 500 para o mês inteiro de outubro/2018 (13-14 discursos da 55ª ficaram fora); o resto da 55ª (349/362) entrou. Os chunks que falham viram falha list no relatório — nada silencioso.
  • Retry por data (retry-c155.sh sobre as datas com falha): 84 datas, recuperou +64 segmentos e +28 trechos; --verify-links 3 com retentativa no probe: 17/18 ok.

Rabbit holes / Não escopo (engenharia)

  • Comissões/audiências e segunda fonte de transcrição (YouTube próprio) — não; youtubeUrl do evento já é metadado, não vira pipeline.
  • Espelhar mídia no S3, gerar clipes, corrigir sessão sem trecho à mão — não.
  • Scheduler/recorrência — não; execução manual documentada.
  • Paralelizar o pipeline ou otimizar wall clock — não (D8-A).
  • Copiar ASR/segmentos do worktree local para produção ou reconciliar ids entre DBs — não (D4-B).
  • Migration/schema novo, access novo, UI — não; se algo exigir, justificar no PR antes de fazer.
  • Commitar JSON de relatório — não (D6).
  • Extrair parseArgs para teste — não agora (D7-D).

Riscos e mitigação

Risco Mitigação
Migration C153 não aplicada em produção (fato: batch 52) → escrita falha Fase 3.0 deploy de main antes de qualquer run; Fase 0 confere migration + tabelas
DEEPINFRA_API_KEY ausente no homeserver (fato) Adicionar ao env file na Fase 0; o modo de escrita sem --skip-transcribe falha cedo e claro
Guard furado pelo proxy 127.0.0.1/host postgres D3: confirm por NODE_ENV=production + host + ALLOW_REMOTE_DB; eco do alvo; smoke de produção com re-run antes do run completo
Queda de rede/SSH durante 10–15h tmux + tee; checkpoint por legislatura; resume idempotente (--all ou --legislature N); falha de uma legislatura não derruba as demais
Disco / enche com MP4/HTML cacheados --out /srv/hdd/teqo-backfill/camara (394GB livres); cache de eventos no mesmo destino
Sessões antigas sem trecho/HTML fora do padrão (54ª inteira, 55ª parcial) Não é falha: vira withoutExcerpt/withoutSegments/fallbackYoutube no coverage e pendência documentada; nenhuma correção manual
Custo acima do orçamento Relatório mede ASR/LLM por legislatura; teto declarado ~US$0,7; --skip-transcribe disponível para re-runs de metadados
Memória do processo (caches eventsByDate/eventPages) em 8c/15GB Monitorar no run; fallback: rodar por --legislature (o checkpoint já é natural)
Escrita concorrente com o app em produção Escrita confinada a speech/speechSegment/rels, transacional, sem locks de outras tabelas; janela de baixa atividade
Reexecução acidental pós-run (tempo/custo) Idempotência + coverage + guard; re-run é seguro, mas exige a flag e custa wall clock
C154 não está em main Dados ficam prontos; validação visual registrada como pendência do C154, não bloqueio deste item
Colisão de sourceKey (vista na 57ª) Já resolvida no C153 por hash de conteúdo; o relatório marca suffixedKey

Débitos (triage pós-simplify)

Nenhum achado dos dois revisores virou Issue nova (nenhum expensive_lock com score ≥4). O que ficou:

Deferido com gatilho:

  • Resolver do pool Postgres duplicado (src/utilities/speech/speechCoverage.ts vs src/utilities/supporter/supporterListOverviewAggregate.ts): os dois checam payload.db.name + drizzle.execute com mensagens de erro próprias. Extrair para drizzleBulk seria abstração com 2 call sites e semânticas de erro distintas. Gatilho: 3º consumidor do resolver ou mudança no shape do adapter.
  • Listas de buckets/chaves soletradas (speechCoverage: tipo/SQL/rowToCoverage/sumCoverage; camaraSpeeches: BACKFILL_*_KEYS vs createRun): derivar dinamicamente de Object.keys esconderia a quebra de contrato (campo novo que some do relatório sem erro). Gatilho: um 8º bucket no coverage ou um 4º bloco numérico no relatório.

Descartado: o par --date + --legislature continua aceito (ignorado, shape do C153 preservado) — o C153 já o aceitava; agora o options.legislature fica null como antes e o HELP diz "ignorado se --date".

Já resolvido no simplify (não reabrir):

  • getJsonWithBackoff movido para scripts/lib/camaraFetch.mjs (o dono do retry HTTP), com label injetável.
  • Guard reordenado (assertWriteAllowed antes do assertLocalDatabase) para o disjunto de host remoto ser alcançável; mensagem de DATABASE_URL ausente em pt-BR.
  • Pipeline por legislatura extraído (runLegislature) e reusado pelo single-range e pelo --all.
  • combined inicializado por aggregateBackfillRuns([]) (sem totals/asr/llm nulos).
  • Bloco por legislatura imprime N falha(s); --verify-links não grava JSON com amostra vazia.
  • databaseTarget com parse único; PostgresDb reusa PostgresTransactionDatabase; JSDoc de selectLinkSample honesto; linha em branco no cli.mjs; nome do artefato do plano alinhado (backfill-<runAt>.json).
  • Teste do ramo uniforme de selectLinkSample (6 linhas, n=2/3, ids exatos).

Aceite de engenharia (checklist)

  • --all processa 54→57 num processo, com legislatures[] + totais combinados + coverage; mutex com --date/--legislature explícito; checkpoint por legislatura.
  • Idempotência comprovada em produção: segunda passada do --all --limit 1 = 0 criados / 0 ASR / 0 LLM.
  • --coverage é read-only no banco e sem rede; imprime por legislatura (total, com trecho, com vídeo, com segmentos, sem trecho, sem segmentos, fallback YouTube) + totais.
  • CAMARA_IMPORT_CONFIRM=1 exigido para escrita com NODE_ENV=production, host remoto ou ALLOW_REMOTE_DB; fail-closed antes de getPayload; --coverage/--verify-links isentos; --skip-transcribe não isento; alvo ecoado.
  • --verify-links <n> amostra n por legislatura, GET Range com fallback HEAD (e uma retentativa), warnings não fatais, 0 verificados = exit 1.
  • Nenhuma escrita fora de speech/speechSegment/speech_rels; nenhum objeto no S3; CC BY 4.0 no runbook/changelog.
  • Unit novos (agregação, amostragem, guard, chunks de data) e int de speechCoverage com fixtures, verdes.
  • pnpm gate:ci (lint/format/typecheck/knip/cycles/unit/int/build) verde; e2e local campaignSpeechAcervo 6/6; CI/PR roda o curado.
  • Produção com 997 discursos (54ª=1, 55ª=348, 56ª=414, 57ª=234) e migrations C153/C154 aplicadas; coverage pós-run bate com o relatório (1 re-run do smoke explica 998 processamentos).
  • --verify-links 3 na amostra sem falha dura (17/18 ok, 1 warning transitório); 77 sem segmentos e 50 sem trecho documentados como pendência de fonte.
  • Relatórios anexados à Issue #957; resumo no corpo do PR; docs/changelog/2026-09-13-c155.md e seção C155 no runbook com resultado e rollback (DELETE em speech_segment/speech_rels/speech, ou re-run idempotente).
  • Nenhuma migration nova; nenhum arquivo de data/camara/ commitado; Closes #957 no PR com auto-merge armado.

Self-score decision-quality

Item Nota Justificativa
Decisões caras (guard de escrita, sequência da operação, contrato do relatório/coverage) decididas explicitamente 5 D3/D4/D2 têm opções, recomendação e rejeitadas; o barato (nomes de campo, n da amostra) ficou como fill-in com gatilho
Reuso dos donos existentes (depth check) 5 import-camara-speeches.mjs, camaraSpeeches/camaraFetch, cli.mjs, speechImport, recover-media/OPS79 como molde, drizzleResultRows — sem twin
Formato Opções/Recomendação/Rejeitadas em todas as decisões 5 D1–D8 no formato obrigatório, com rejeitadas justificadas (não decorativas)
Fases verificáveis com quota e prova por fase 4 Cada fase tem quota e prova; a estimativa de wall clock (10–15h) é probabilística e a fase 3 depende do deploy manual
Riscos com mitigação e rollback idempotente 4 12 riscos mapeados com mitigação; rollback por DELETE/re-run documentado, mas depende de operador humano no homeserver

Média: 4,6 (≥4).