10 KiB
Impl: Acervo cortes — dívida pós-C174: select enxuto dos loaders, teto do id in da origem e paridade busca↔VM
Status: aprovado Atualizado em: 2026-09-16 Issue: #1097 Intenção: docs/plans/acervo-cortes-divida-pos-c174.md Appetite restante: herdado (~0,5–1 dia)
Leitura da intenção
- Outcome: cortar o custo de query dos três loaders de corte e o risco de drift entre a busca (SQL) e o VM, sem mudar comportamento visível.
- O que NÃO negociar: nenhuma superfície visual nova;
lib/nunca importautilities/; sem módulo gêmeo; sem migration;loadSpeechCutAcervoPageData(depth 1),loadSpeechCutDetailPageData/findSpeechCutForActore/corte/<id>intocados. - O que reavaliar (surpresas do explorer):
SpeechNestedCutscomshowDescriptionrenderizadescription(SpeechNestedCuts.tsx:62-64) → o DTO estreito PRECISA dedescription.- As superfícies usam
durationSeconds+formatSpeechClock, nãodurationLabel(formatSpeechSpan) → manterdurationSeconds; NÃO incluirdurationLabel. failureMessage/error/stepnão são lidos por nenhuma das duas superfícies → saem do DTO; mas a normalizaçãostatusdesconhecido →'failed'fica (senãoSpeechCutStatusBadgequebra).tests/int/speechCut.int.spec.ts:860-870assertamediaUrl/publicPathno retorno desse loader → muda com o novo shape.
Abordagem recomendada
flowchart LR
L[loaders: select enxuto + limit] --> S[toSpeechCutSummaryViewModel]
S --> A[SpeechCutsForSpeechSection]
S --> B[SpeechNestedCuts + SpeechListItemViewModel.cuts]
P[lib/speechSearch: predicado puro] --> VM[speechViewModels.speechMatchesSearchQuery]
P -. mesma semântica .-> W[speechListFilters Where]
C[SPEECH_CUT_ORIGIN_SPEECH_ID_LIMIT + originSpeechIdsOfCuts] --> L
Opções consideradas: A — DTO estreito + predicado puro compartilhado + cap do id in (as três fases) | B — só adicionar select mantendo toSpeechCutViewModel completo | C — F3 via subquery/raw e F2 via comparar por id já resolvido.
Recomendação: A — remove o payload não usado no shape (não só no tráfego), unifica a semântica num só lugar puro e fecha o in com degradação honesta; menor caminho que cobre as três fases.
Rejeitadas: B — select enxuto com VM completo ainda mapeia mediaUrl/youtubeVideoId/failureMessage que ninguém lê e exigiria error no select; C — subquery exige SQL cru fora do repositório Payload (quebra depth) e comparar por id erra quando a fala casa por texto E é origem de um corte.
Componentes / mudanças
SpeechCutSummaryViewModel+toSpeechCutSummaryViewModel+cutDurationSeconds(src/lib/speechCut.ts): mapper puro que emite{ id, status, title, description, durationSeconds }, reusando o fallbackdurationSeconds ?? end − start(extraído detoSpeechCutViewModel) e a normalização de status.toSpeechCutViewModelpassa a usar o helper extraído.speechMatchesSearchQuery(src/lib/speechSearch.ts): predicado puro do ramo textual;speechViewModelsdeixa de terspeechMatchesTextlocal e chama este.SPEECH_CUT_ORIGIN_SPEECH_ID_LIMIT+originSpeechIdsOfCuts(src/lib/speechCut.ts): dedupe + cap puro dos ids de fala de origem.speechCutPageData.ts:speechCutSummarySelect(status/title/description/durationSeconds/startSeconds/endSeconds) nos dois loaders de corte;speechCutOriginSelect(speech) +limitna origem; retornos passam aSpeechCutSummaryViewModel; origem usaoriginSpeechIdsOfCuts.speechViewModels.ts/SpeechCutsForSpeechSection.tsx/SpeechNestedCuts.tsx: tipos de prop passam aSpeechCutSummaryViewModel(render inalterado).- Migration: sem migration — nenhuma collection/global/field muda; só
select,limite DTO de leitura. - Access / Consent: n/a — os loaders seguem
user+overrideAccess:false; nenhum write path. - UI: Impeccable A; nenhuma superfície visual nova nem copy nova → nenhum trigger do
designer(a/b/c/d) é acionado.
Dados → forma
- Não se aplica (sem KPI/série/mapa; o dado já é lista).
Fases verificáveis
- F1 — DTO estreito +
select(lib/speechCut.ts,speechCutPageData.ts, 3 componentes). Prova:tests/unit/speechCut.unit.spec.tsnovodescribe('toSpeechCutSummaryViewModel')(chaves exatas semmediaUrl/failureMessage; fallbackend − start; status desconhecido →failed); intspeechCut.int.spec.ts:860-870atualizado para as chaves do summary. - F2 — predicado único (
lib/speechSearch.ts,speechViewModels.ts). Prova:tests/unit/speechSearch.unit.spec.tsdescribe('speechMatchesSearchQuery')(searchText acento/caixa; keyword case-insensitive; query vazia/undefined→ true) + int novo emspeechCut.int.spec.tscomkeywordsem caixa diferente doq(o falso "Fala de origem" de hoje viramatchedTextSearch: true). - F3 — cap do
id in(lib/speechCut.ts,speechCutPageData.ts). Prova: unit deoriginSpeechIdsOfCuts(dedupe, relação não resolvida, corta nolimit); int existentespeechCut.int.spec.ts:883-915mantém a fiação ponta-a-ponta. - Gates:
pnpm gate:fastna iteração; entrega compnpm push. E2E local discricionário; o manifest dispara o conjunto curadocampaignSpeechAcervo+campaignSpeechCut(scripts/lib/e2e-affected-manifest.mjs:300-314) e o CI/PR cobre o blast radius.
Rabbit holes / Não escopo (engenharia)
- Não estreitar
loadSpeechCutAcervoPageData(speechCutPageData.ts:42-70, depth 1 precisa de origem/media) nemloadSpeechCutDetailPageData/findSpeechCutForActor. - Não tocar
toSpeechCutViewModelalém de extrair o helper de duração; dialog/player/result card seguem com o VM completo. - Não mexer no índice/
searchTextdespeechCut(gatilho de volume do C168) nem no renamematchedTextSearch. - Não introduzir
durationLabel/formatSpeechSpannas duas superfícies (mudaria a saída). - Lista fixa de "Explicitamente fora" da intenção permanece fechada.
Riscos e mitigação
- Select enxuto quebrar o VM: o summary não lê
media/error/step; risco morre por construção — unit do mapper + int de chaves. - Predicado divergir do SQL: documentar a semântica (
searchTextnormalizado dos dois lados;keywordscase-insensitive mas acento-significante, como o ILIKE%q%) e fixar com unit + int. - Cap degradar a busca: o cap só corta a superfície "fala de origem" (menos falas origin-only), nunca torna resultado errado — o flag vem do predicado, não dos ids. Volume-gatilho ~5–10 mil cortes.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto (nenhuma saída visual muda)
- Invariantes AGENTS/engineering-standards (depth
lib/→utilities/, sem top-level novo, sem migration, pt-BR/identificadores EN) - Testes de domínio previstos (unit dos mappers/predicado/cap + int de loader) — sem write path novo
Decisões de engenharia
- D1 — DTO: novo
SpeechCutSummaryViewModel+toSpeechCutSummaryViewModelemlib/speechCut.ts. Incluidescription(exigido peloshowDescriptiondoSpeechNestedCuts), mantémdurationSeconds(a superfície usaformatSpeechClock) e a normalizaçãostatusdesconhecido →'failed'(senão o badge quebra). Rejeitados: mantertoSpeechCutViewModelcompleto (mapeia campos não lidos) e um DTO semdescription(perde a evidência da CENA 05). SaemfailureMessage/step/media*/publicPath/youtubeVideoId/publishedAt. - D2 — predicado único:
speechMatchesSearchQuerypuro emsrc/lib/speechSearch.ts(lib não importa utilities; ambos os consumidores já importamnormalizeForSearch), consumido pelo VM; owheresegue única fonte do SQL.searchTextcasa normalizado/acento-insensível dos dois lados;keywordspassa atoLowerCase().includes()para casar o ILIKE (case-insensitive) mantendo acento significante — corrige o falso "Fala de origem" de hoje. Rejeitados: comparar por id já resolvido (erra quando a fala casa por texto E é origem) e duplicar o predicado. - D3 — teto:
SPEECH_CUT_ORIGIN_SPEECH_ID_LIMIT = 200comolimitda query de origem + cap nooriginSpeechIdsOfCuts; degradação honesta = menos resultados "só por corte", nunca resultado errado. Rejeitadas: subquery/raw (fora do Payload, quebra depth) e deixar sem teto (oid incresce sem limite com termo comum; gatilho ~5–10 mil cortes/C168).
Explicitamente fora / Adiado com gatilho (triage do /simplify)
- Select ↔ mapper ligados por convenção (S1):
speechCutSummarySelecteSpeechCutSummaryRecordsão mantidos em passo por comentário cruzado + o int exact-keys; um campo novo sem select key faz o int/unit falharem (não éundefinedsilencioso). Link compile-time (chave tipada derivando os dois) fica adiado — gatilho: o mapper ganhar um 2º consumidor ou o int deixar de cobrir o shape. - Paridade predicado ↔
wheresó por teste de branch (S6):speechListFilters.unit.spec.tsprende o conjunto exato de branches (toEqual), então adicionar/remover branch falha e força revisitar o predicado (doc aponta o par). Teste de paridade automático adiado — gatilho: uma 3ª dimensão textual entrar na busca de falas. originSpeechIdsOfCutscom 1 call site (S3): mantido como dono puro do cap/dedupe (testável sem DB); olimitdefault duplica o da query de propósito (invariante). Gatilho para reavaliar: 3º call site ou 2º loader precisar dos ids de origem.- Int exact-keys (S10): mantido de propósito — é o único guard do contrato loader-select↔mapper (S1); reavaliar só se S1 ganhar link compile-time.
- Já resolvido no simplify (não reabrir):
SpeechCutSummaryRecordviaPick(S2); doc dospeechSearch(S4); JSDoc dooriginSpeechIdsOfCuts(S5); export morto do record (S7); off-by-one delimit: 0(S8); tipoCutDurationRecord & Pick(S9); nome/fronteira do teste (S11).
Self-score (decision-quality ≥4)
- Decisões caras com rejeitadas — 4/5: D1–D3 nomeiam opção, porquê e rejeitadas, com shape de DTO, semântica de ILIKE e teto resolvidos.
- Cabe no appetite — 5/5: 5 arquivos de produção + 5 de teste; reusa mappers/loaders/int existentes.
- Rabbit holes nomeados — 5/5: lista fixa herdada + os loaders que NÃO podem ser estreitados.
- Depth check — 5/5:
utilities/speech/segue dono; predicado e mappers puros emlib/; nenhum gêmeo. - Intenção de produto — 5/5: zero mudança visível; só custo de query, paridade e teto.
Score: 5/5 (mínimo 4).