TS98 linhas6.240 bytes
apps/worker/scripts/auditar-tramitacao.ts
SHA-256
40baa16da6f444f84f13e6ced249547e20aab9bed262635f0b93b9ed5fb06386
Somente leiturafonte-0531042d9f3e
/**
* Auditoria pública, sem credenciais, banco ou cache de produção.
* pnpm --filter worker exec tsx scripts/auditar-tramitacao.ts [--limite=8]
* Sem limite: toda a coorte. Só mede efetividade, não recalcula o IDS completo.
* --cache-publico=/tmp/auditoria permite retomar consultas em caso de falha.
*/
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { FsKV } from '../src/runtime/node/kv-fs'
import { getSenadorList, MESES_MIN_PREMIO } from '../src/services/legis'
import { avaliarAutorias, corteProposicoes } from '../src/services/processos'
import { PESO_PROPOSICAO, extrairSigla, resumirAutorias } from '../src/services/proposicoes'
import type { AutoriaAvaliada } from '../src/services/proposicoes'
import type { Env } from '../src/types'
function ranks(v: number[]): number[] {
return v.map((x) => 1 + v.filter((y) => y > x + 1e-12).length + (v.filter((y) => Math.abs(y - x) < 1e-12).length - 1) / 2)
}
function correlacao(a: number[], b: number[]): number | null {
const media = (v: number[]) => v.reduce((x, y) => x + y, 0) / v.length
const ma = media(a), mb = media(b)
const cov = a.reduce((s, v, i) => s + (v - ma) * (b[i] - mb), 0)
const den = Math.sqrt(a.reduce((s, v) => s + (v - ma) ** 2, 0) * b.reduce((s, v) => s + (v - mb) ** 2, 0))
return den ? Math.round(1e4 * cov / den) / 1e4 : null
}
async function main() {
const cacheEscolhido = process.argv.find((a) => a.startsWith('--cache-publico='))?.slice('--cache-publico='.length)
const pasta = cacheEscolhido || await mkdtemp(join(tmpdir(), 'observasenado-auditoria-'))
const env = { LEGIS_BASE_URL: 'https://legis.senado.leg.br/dadosabertos', SENADO_CACHE: new FsKV(pasta) } as unknown as Env
const limite = Number(process.argv.find((a) => a.startsWith('--limite='))?.split('=')[1] ?? Infinity)
const log = console.log
console.log = (...args) => console.error(...args) // stdout é somente o relatório JSON
try {
const coorte = (await getSenadorList(env)).sort((a, b) => Number(a.codigo) - Number(b.codigo)).slice(0, limite)
const dados: Array<{ codigo: string; elegivel: boolean; autorias: AutoriaAvaliada[] }> = []
for (let i = 0; i < coorte.length; i += 3) {
const lote = await Promise.allSettled(coorte.slice(i, i + 3).map(async (s) => ({ codigo: s.codigo, elegivel: (s.mesesAtivos ?? 0) >= MESES_MIN_PREMIO, autorias: await avaliarAutorias(env, s.codigo) })))
const erro = lote.find((r): r is PromiseRejectedResult => r.status === 'rejected')
if (erro) throw erro.reason
dados.push(...lote.map((r) => (r as PromiseFulfilledResult<typeof dados[number]>).value))
console.error(`Auditados ${dados.length}/${coorte.length}`)
}
const cenarios = [
{ nome: 'linear_K12', pontos: [0, 1, 2, 3, 4], k: 12, tipos: true },
{ nome: 'mais_peso_norma', pontos: [0, 0.5, 1, 2, 4], k: 12, tipos: true },
{ nome: 'mais_peso_intermediario', pontos: [0, 1.5, 2.5, 3.5, 4], k: 12, tipos: true },
{ nome: 'linear_K6', pontos: [0, 1, 2, 3, 4], k: 6, tipos: true },
{ nome: 'linear_K24', pontos: [0, 1, 2, 3, 4], k: 24, tipos: true },
{ nome: 'tipos_iguais', pontos: [0, 1, 2, 3, 4], k: 12, tipos: false },
]
const comparacoes = cenarios.map((c) => {
const brutos = dados.map((d) => {
const propostas = d.autorias.filter((a) => a.tramitacao.normativa)
const pesos = propostas.map((a) => c.tipos ? PESO_PROPOSICAO[extrairSigla(a.processo)] ?? 0.5 : 1)
const soma = pesos.reduce((a, b) => a + b, 0)
const pontos = propostas.reduce((s, a, i) => s + pesos[i] * c.pontos[a.tramitacao.pontos], 0)
return { taxa: soma ? pontos / (4 * soma) : 0, n: soma ? soma ** 2 / pesos.reduce((s, p) => s + p * p, 0) : 0 }
})
const referencia = brutos.filter((b, i) => dados[i].elegivel && b.n > 0)
const media = referencia.reduce((s, b) => s + b.taxa, 0) / (referencia.length || 1)
return { nome: c.nome, valores: brutos.map((b) => b.n ? (b.taxa * b.n + media * c.k) / (b.n + c.k) : 0) }
})
const sensibilidade = (incluir: (i: number) => boolean) => {
const base = ranks(comparacoes[0].valores.filter((_, i) => incluir(i)))
const top = new Set(base.flatMap((r, i) => r <= 10 ? [i] : []))
return comparacoes.map((c) => {
const r = ranks(c.valores.filter((_, i) => incluir(i)))
const deslocamentos = r.map((x, i) => Math.abs(x - base[i]))
return {
cenario: c.nome, spearman: correlacao(base, r),
maiorDeslocamento: r.length ? Math.max(...deslocamentos) : 0,
deslocamentoMedio: r.length ? Math.round(100 * deslocamentos.reduce((a, b) => a + b, 0) / r.length) / 100 : 0,
mantidosNoTop10: r.filter((x, i) => x <= 10 && top.has(i)).length,
}
})
}
const resumos = dados.map((d) => ({ codigo: d.codigo, elegivel: d.elegivel, ...resumirAutorias(d.autorias, corteProposicoes()) }))
const unicos = new Set(dados.flatMap((d) => d.autorias.map((a) => String(a.processo.idProcessoCasaInicial || a.processo.id))))
const report = {
coletadoEm: new Date().toISOString(), dataCorte: corteProposicoes(), senadores: dados.length, projetosUnicos: unicos.size,
elegiveis: dados.filter((d) => d.elegivel).length,
normativasPorEtapa: Object.fromEntries(Object.keys(resumos[0]?.tramitacao.porEtapa ?? {}).map((etapa) => [etapa, resumos.reduce((n, r) => n + r.tramitacao.porEtapa[etapa as keyof typeof r.tramitacao.porEtapa], 0)])),
notaContagens: 'Contagens de autoria: o mesmo projeto pode ter vários coautores. Etapas exclusivas dentro de cada autoria.',
sensibilidade: sensibilidade(() => true),
sensibilidadeElegiveis: sensibilidade((i) => dados[i].elegivel),
resumos,
limites: 'Comparação descritiva da efetividade, não validação causal, nem simulação do IDS total. Pesos não foram escolhidos para favorecer posições individuais.',
}
log(JSON.stringify(report, null, 2))
} finally {
console.log = log
if (!cacheEscolhido) await rm(pasta, { recursive: true, force: true, maxRetries: 3 })
}
}
main().catch((e) => { console.error(e); process.exitCode = 1 })