mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
fix(C160): normaliza coleções obsoletas (J/vazia) do link legado para D
A varredura de produção (2026-09-14) achou 33 falas em selCodColecaoCsv=J e 1 com o parâmetro vazio: o endpoint devolve 'Documento não encontrado' e a mesma data/página resolve na coleção D que a busca atual da Câmara gera. Aliases J/vazio → D, puros e unit-testados; DCN e demais códigos ficam como estão.
This commit is contained in:
parent
e7369839aa
commit
03d1b79629
4 changed files with 115 additions and 1 deletions
|
|
@ -523,6 +523,67 @@ DELETE FROM "speech" WHERE "legislature" = '55';
|
|||
Para desfazer o acervo inteiro: `DELETE FROM "speech_segment"; DELETE FROM "speech";`
|
||||
(a cobertura `--coverage` volta a zero). Reexecutar `--all` reconstrói.
|
||||
|
||||
## C160 — reparo do link oficial do acervo (PDF direto do Diário)
|
||||
|
||||
Operação de dados do acervo de falas (C153/C154/C155): troca o
|
||||
`officialTextUrl` legado (`dc_20b.asp`/`dc_20.asp`/`montaPdf.asp`, que pendura
|
||||
no browser) pelo PDF direto do Diário
|
||||
(`https://imagem.camara.leg.br/Imagem/d/pdf/<arquivo>#page=<página>`). É
|
||||
script-only (nenhuma migration): resolve uma vez por publicação
|
||||
(`<out>/diario/`), é idempotente e reporta cobertura (falas/datas atualizadas,
|
||||
falhas e legado restante).
|
||||
|
||||
**Pré-requisitos:** o SHA do branch/commit com o C160 (a passada original rodou
|
||||
de `~/teqo-backfill` no SHA `766e4ae9`, 2026-09-14); `~/stack/.env` +
|
||||
`~/stack/teqo-1313.env`; `~/teqo-backfill` com `pnpm install` no SHA. O
|
||||
`git fetch` do homeserver alcança o GitHub pelo bundle/com o remote disponível
|
||||
(na passada original o clone apontava para repo local e usou-se `git fetch
|
||||
~/c160.bundle <branch>`).
|
||||
|
||||
```bash
|
||||
ssh homeserver
|
||||
source ~/.nvm/nvm.sh
|
||||
cd ~/teqo-backfill && git fetch origin && git checkout <SHA> && pnpm install
|
||||
set -a; source ~/stack/.env; set +a
|
||||
set -a; source ~/stack/teqo-1313.env; set +a
|
||||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
|
||||
export CAMARA_IMPORT_CONFIRM=1 # guard C160: escrita em produção
|
||||
|
||||
# dry-run (read-only; aquece o cache por publicação em <out>/diario):
|
||||
pnpm camara:import --repair-links --dry-run --out /srv/hdd/backups/teqo-camara
|
||||
# escrita (idempotente; 2ª passada = 0 atualizações):
|
||||
pnpm camara:import --repair-links --out /srv/hdd/backups/teqo-camara
|
||||
# validação (0 legadas restantes):
|
||||
pnpm camara:import --repair-links --dry-run --out /srv/hdd/backups/teqo-camara
|
||||
```
|
||||
|
||||
O guard `CAMARA_IMPORT_CONFIRM=1` é exigido sempre que a escrita não é
|
||||
provadamente local (`NODE_ENV=production`, host remoto ou `ALLOW_REMOTE_DB`);
|
||||
`--dry-run` é read-only e não exige a flag. A escrita é confinada a
|
||||
`speech.officialTextUrl`; VOD, transcrições e segmentos não são tocados.
|
||||
|
||||
**Comportamento medido da Câmara (2026-09-14):** o legado `.gov.br` responde
|
||||
302 para `.leg.br` (hop lento, 25–135s) e o `.leg.br` responde 302 para
|
||||
`montaPdf.asp?narquivo=…&npagina=…` (~5s); o PDF direto responde 200/206
|
||||
`application/pdf` em <1s. O resolver reescreve `.gov.br`→`.leg.br` no lookup,
|
||||
segue o redirect e parseia o `montaPdf`; a resolução é cacheada uma vez por
|
||||
publicação (`diario/<data>-<coleção>[-s<suplemento>].json`) e cada PDF é
|
||||
provado com GET Range antes de gravar. Falha de resolução deixa a fala
|
||||
intocada, o run sai 1 e a próxima passada é idempotente.
|
||||
|
||||
**Resultado registrado (2026-09-14):** 965 falas legadas em 510 publicações
|
||||
resolvidas; 0 legadas restantes na validação; 32 falas sem link (fallback
|
||||
YouTube/oculto). Relatórios em
|
||||
`/srv/hdd/backups/teqo-camara/reports/repair-links-*.json` e changelog
|
||||
`docs/changelog/2026-09-14-c160.md`.
|
||||
|
||||
### Rollback
|
||||
|
||||
Não há migração; o reparo só reescreve `speech.officialTextUrl` e o valor
|
||||
anterior de cada fala está em `updates[].previousUrl` no JSON do run. O import
|
||||
(`pnpm camara:import --date <dia>`) reconstrói o link a partir da API (a fonte
|
||||
é a URL legada devolvida pela Câmara); reexecutar o reparo é idempotente.
|
||||
|
||||
## Referências
|
||||
|
||||
- `scripts/deploy-homeserver.sh` — o script (fonte da verdade do fluxo; parametrizado por `TEQO_ENV`)
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ Sem migration, sem schema, sem access, sem UI, sem package.json (`pnpm camara:im
|
|||
- Refatorar `parseArgs` inteiro ou promover o resolver a utility `src/` — **não**; dono é `scripts/`.
|
||||
- Commitar cache/relatórios — **não** (`data/camara/` gitignored).
|
||||
|
||||
### Adiado com gatilho (triage do simplify)
|
||||
|
||||
- **Conversão `montaPdf` duplicada** (2 call sites: `resolveOfficialTextUrl` e `planOfficialLinkRepairs`): extrair um helper puro no lib **quando um 3º call site aparecer** ou a regra mudar.
|
||||
- **Wiring do fallback do import** (preservar PDF direto anterior em falha de resolução) coberto pelo unit de `officialTextUrlFallback`, não pela closure do script: extrair o resolvedor com deps injetadas **se o caminho de falha ganhar um modo novo** ou o fallback for modificado.
|
||||
- **Sanitize da cache key** (`publicationCacheKey`) funde códigos de coleção com caracteres não previstos (`_`): revisitar **se aparecer coleção real fora de `[A-Za-z0-9]`**.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
| Risco | Mitigação |
|
||||
|
|
|
|||
|
|
@ -506,6 +506,22 @@ const queryValue = (url, ...names) => {
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The open-data API still emits stale `selCodColecaoCsv` codes for speeches
|
||||
* whose Diário is the Câmara collection: `J` and empty return "Documento não
|
||||
* encontrado" (measured 2026-09-14) while the same date/page resolves under the
|
||||
* current `D` code the Câmara search itself generates. Any other code (e.g.
|
||||
* `DCN`) is kept as-is.
|
||||
*/
|
||||
const LEGACY_COLLECTION_ALIASES = { '': 'D', J: 'D' }
|
||||
|
||||
const normalizeLegacyCollection = (raw) => {
|
||||
const value = String(raw ?? '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
return LEGACY_COLLECTION_ALIASES[value] ?? value
|
||||
}
|
||||
|
||||
const positiveIntegerOrNull = (value) => {
|
||||
const number = Number(value)
|
||||
return Number.isInteger(number) && number > 0 ? number : null
|
||||
|
|
@ -565,13 +581,20 @@ export const parseLegacyOfficialUrl = (raw) => {
|
|||
}
|
||||
const date = parseDiarioDate(queryValue(url, 'Datain'))
|
||||
if (date === null) return null
|
||||
const rawCollection = queryValue(url, 'selCodColecaoCsv', 'selCodColecao')
|
||||
const collection = normalizeLegacyCollection(rawCollection)
|
||||
const lookupUrl = new URL(url)
|
||||
if (lookupUrl.hostname === 'imagem.camara.gov.br') lookupUrl.hostname = 'imagem.camara.leg.br'
|
||||
if (collection !== rawCollection) {
|
||||
lookupUrl.searchParams.delete('selCodColecaoCsv')
|
||||
lookupUrl.searchParams.delete('selCodColecao')
|
||||
lookupUrl.searchParams.set('selCodColecaoCsv', collection)
|
||||
}
|
||||
return {
|
||||
lookupUrl: lookupUrl.toString(),
|
||||
publication: {
|
||||
date,
|
||||
collection: queryValue(url, 'selCodColecaoCsv', 'selCodColecao'),
|
||||
collection,
|
||||
supplement: queryValue(url, 'txSuplemento'),
|
||||
},
|
||||
page: positiveIntegerOrNull(queryValue(url, 'txPagina')),
|
||||
|
|
|
|||
|
|
@ -737,6 +737,30 @@ describe('parseLegacyOfficialUrl', () => {
|
|||
expect(parsed?.page).toBeNull()
|
||||
})
|
||||
|
||||
it('normalizes the stale J and empty collection codes to D', () => {
|
||||
const staleJ =
|
||||
'https://imagem.camara.gov.br/dc_20b.asp?largura=&altura=&tipoForm=diarios' +
|
||||
'&selCodColecaoCsv=J&Datain=10%2F10%2F2019&txPagina=147&txSuplemento=&enviar=Pesquisar'
|
||||
const parsedJ = parseLegacyOfficialUrl(staleJ)
|
||||
expect(parsedJ?.publication).toEqual({ date: '2019-10-10', collection: 'D', supplement: null })
|
||||
expect(parsedJ?.lookupUrl).toContain('selCodColecaoCsv=D')
|
||||
expect(parsedJ?.lookupUrl).not.toContain('selCodColecaoCsv=J')
|
||||
|
||||
const parsedEmpty = parseLegacyOfficialUrl(
|
||||
staleJ.replace('selCodColecaoCsv=J', 'selCodColecaoCsv='),
|
||||
)
|
||||
expect(parsedEmpty?.publication.collection).toBe('D')
|
||||
expect(parsedEmpty?.lookupUrl).toContain('selCodColecaoCsv=D')
|
||||
})
|
||||
|
||||
it('keeps a collection that is not a known stale alias', () => {
|
||||
const parsed = parseLegacyOfficialUrl(
|
||||
legacyGovUrl.replace('selCodColecaoCsv=D', 'selCodColecaoCsv=S'),
|
||||
)
|
||||
expect(parsed?.publication.collection).toBe('S')
|
||||
expect(parsed?.lookupUrl).toContain('selCodColecaoCsv=S')
|
||||
})
|
||||
|
||||
it('returns null for non-legacy or dateless URLs', () => {
|
||||
expect(parseLegacyOfficialUrl(directPdfUrl)).toBeNull()
|
||||
expect(parseLegacyOfficialUrl('https://imagem.camara.leg.br/dc_20b.asp?Datain=xx')).toBeNull()
|
||||
|
|
|
|||
Loading…
Reference in a new issue