TS206 linhas9.761 bytes
apps/worker/src/services/proposicoes.ts
SHA-256
3fda5c4047d5b59a923a19a6b0ecfcd797a6a3a1542baa6ffa8c79757d6d7f79
Somente leiturafonte-0531042d9f3e
/** IDS v5: avanços documentados, com corte temporal e uma contribuição por projeto. */
export const IDS_VERSAO = 'v5'
export const RAW_CACHE_PREFIX = 'raw:v5'
export const PROCESSOS_CACHE_PREFIX = 'processos:v5'
export const PESO_PROPOSICAO: Record<string, number> = {
PEC: 3, PLP: 2, PL: 1.5, PLN: 1.5, PLS: 1.5, PLC: 1.5,
PDL: 1.2, PDS: 1.2, PRS: 1, PRN: 1, MPV: 1,
RQS: 0.4, RQN: 0.4, REQ: 0.4, INC: 0.2, IND: 0.2,
}
const NORMATIVAS = new Set(['PEC', 'PLP', 'PL', 'PLN', 'PLS', 'PLC', 'PDL', 'PDS', 'PRS', 'PRN'])
// Incrementos iguais são uma convenção transparente, não coeficientes estimados.
// Aprovação no Senado e envio à Câmara são o MESMO marco: não somar os dois.
export const PONTOS_ETAPA = {
sem_avanco: 0, instrucao: 1, aprovado_senado: 2, camara: 2, sancao: 3, norma: 4,
} as const
export type EtapaTramitacao = keyof typeof PONTOS_ETAPA
export const ROTULO_ETAPA: Record<EtapaTramitacao, string> = {
sem_avanco: 'Sem avanço comprovado',
instrucao: 'Avanço deliberativo',
aprovado_senado: 'Aprovação no Senado',
camara: 'Envio à Câmara',
sancao: 'Envio à sanção ou promulgação',
norma: 'Transformação em norma',
}
type Registro = Record<string, unknown>
const obj = (v: unknown): Registro => v && typeof v === 'object' && !Array.isArray(v) ? v as Registro : {}
const lista = (v: unknown): Registro[] => Array.isArray(v) ? v.map(obj) : []
const texto = (v: unknown): string => typeof v === 'string' ? v : ''
const normalizar = (v: unknown) => texto(v).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase().trim()
const dataISO = (v: unknown): string | undefined => {
const s = texto(v).slice(0, 10)
return /^\d{4}-\d{2}-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s ? s : undefined
}
export function extrairSigla(p: Registro): string {
return normalizar(p.sigla) || normalizar(p.identificacao).match(/^([A-Z]+)\s/)?.[1] || ''
}
export function ehNormativa(p: Registro): boolean {
return NORMATIVAS.has(extrairSigla(p))
}
export interface EvidenciaTramitacao {
etapa: EtapaTramitacao
data: string
fonte: 'situacao' | 'deliberacao' | 'norma'
descricao: string
}
export interface Tramitacao {
normativa: boolean
etapa: EtapaTramitacao
pontos: number
rotulo: string
dataCorte: string
evidencias: EvidenciaTramitacao[]
evidenciasSemData: number
}
const APROVACOES = new Set(['APROVADA_NO_PLENARIO', 'APROVADA_EM_COMISSAO_TERMINATIVA', 'APROVADA_PARCIALMENTE', 'APROVADO_NA_INTEGRA'])
const SITUACOES: Record<string, EtapaTramitacao> = {
PRONDEPLEN: 'instrucao', INCLOD: 'instrucao', AGDDO: 'instrucao',
APRVD: 'aprovado_senado', 'APRVD(DT)': 'aprovado_senado', APRVDP: 'aprovado_senado',
RMCD: 'camara', AGCD: 'camara', RMSAN: 'sancao', RMPRO: 'sancao',
TNJR: 'norma', TNJRVETO: 'norma',
}
const DESCRICOES: Record<string, EtapaTramitacao> = {
'PRONTO PARA DELIBERACAO DO PLENARIO': 'instrucao',
'INCLUIDA EM ORDEM DO DIA': 'instrucao',
'AGENDADA PARA ORDEM DO DIA': 'instrucao',
'APROVADA': 'aprovado_senado',
'APROVADA A MATERIA (DECISAO TERMINATIVA)': 'aprovado_senado',
'APROVADA PARCIALMENTE': 'aprovado_senado',
'REMETIDA A CAMARA DOS DEPUTADOS': 'camara',
'AGUARDANDO DECISAO DA CAMARA DOS DEPUTADOS': 'camara',
'REMETIDA A SANCAO': 'sancao', 'REMETIDA A PROMULGACAO': 'sancao',
'TRANSFORMADA EM NORMA JURIDICA': 'norma',
'TRANSFORMADA EM NORMA JURIDICA COM VETO PARCIAL': 'norma',
}
/**
* Lê somente campos oficiais do próprio processo e da autuação principal.
* Ementa, objetivo, requerimentos incidentais e simples `tramitando=Sim` não
* comprovam avanço. Arquivamento posterior preserva os marcos já documentados.
* Datas posteriores ao corte (ou ausentes) nunca pontuam uma edição histórica.
*/
export function classificarTramitacao(p: Registro, corte: string): Tramitacao {
const evidencias: EvidenciaTramitacao[] = []
let evidenciasSemData = 0
const normativa = ehNormativa(p)
const add = (etapa: EtapaTramitacao | undefined, data: unknown, fonte: EvidenciaTramitacao['fonte'], descricao: unknown) => {
if (!etapa || !normativa) return
const dt = dataISO(data)
if (!dt) { evidenciasSemData++; return }
if (dt > corte) return
const ev = { etapa, data: dt, fonte, descricao: texto(descricao) }
if (!evidencias.some((x) => x.etapa === etapa && x.data === dt && x.fonte === fonte)) evidencias.push(ev)
}
const situacao = (s: Registro) => {
const sigla = normalizar(s.sigla)
add(SITUACOES[sigla] ?? DESCRICOES[normalizar(s.descricao)], s.inicio, 'situacao', s.descricao || sigla)
}
situacao({ sigla: p.siglaSituacaoAtual, descricao: p.situacaoAtual, inicio: p.dataSituacaoAtual })
for (const a of lista(p.autuacoes).filter((a) => Number(a.numero) === 1)) {
for (const s of lista(a.situacoes)) situacao(s)
// APRVPAR sozinho também pode registrar parecer pela rejeição. Exigir
// indicação favorável no informe associado ao mesmo evento oficial.
for (const i of lista(a.informesLegislativos)) {
const descricao = normalizar(i.descricao)
if (i.siglaSituacaoIniciada === 'APRVPAR' &&
/PELA APROVACAO|FAVORAVEL (?:AO|A) (?:PROJETO|MATERIA|PROPOSTA)/.test(descricao) &&
!/PELA REJEICAO|PELA PREJUDICIALIDADE|CONTRARIO (?:AO|A) (?:PROJETO|MATERIA|PROPOSTA)/.test(descricao)) {
add('instrucao', i.data, 'situacao', 'Parecer favorável aprovado na comissão')
}
}
}
const delib = obj(p.deliberacao)
const siglaDelib = normalizar(delib.siglaTipo || p.siglaTipoDeliberacao)
const dataDelib = delib.data || p.dataDeliberacao
if (APROVACOES.has(siglaDelib)) {
add('aprovado_senado', dataDelib, 'deliberacao', delib.tipoDeliberacao || siglaDelib)
const destino = normalizar(delib.siglaDestino)
if (destino === 'CAMARA') add('camara', dataDelib, 'deliberacao', 'À Câmara dos Deputados')
if (destino === 'SANCAO' || destino === 'PROMULGACAO') add('sancao', dataDelib, 'deliberacao', delib.destino)
}
const norma = obj(p.normaGerada)
if (Number(norma.codigo) > 0 || (Number(norma.numero) > 0 && norma.siglaTipo)) {
add('norma', norma.dataAssinatura || norma.dataPublicacao, 'norma', norma.descricao)
} else if (typeof p.normaGerada === 'string' && /^(Lei|Emenda Constitucional|Decreto Legislativo|Resolu[çc][ãa]o)\b/i.test(p.normaGerada)) {
const dt = p.normaGerada.match(/\b(\d{2})\/(\d{2})\/(\d{4})\b/)
add('norma', dt ? `${dt[3]}-${dt[2]}-${dt[1]}` : undefined, 'norma', p.normaGerada)
}
const ordem = Object.keys(PONTOS_ETAPA) as EtapaTramitacao[]
evidencias.sort((a, b) => a.data.localeCompare(b.data) || ordem.indexOf(a.etapa) - ordem.indexOf(b.etapa))
const etapa = evidencias.reduce<EtapaTramitacao>((maior, e) => ordem.indexOf(e.etapa) > ordem.indexOf(maior) ? e.etapa : maior, 'sem_avanco')
return { normativa, etapa, pontos: PONTOS_ETAPA[etapa], rotulo: ROTULO_ETAPA[etapa], dataCorte: corte, evidencias, evidenciasSemData }
}
export interface ResumoTramitacao {
normativas: number
porEtapa: Record<EtapaTramitacao, number>
pontosPonderados: number
pesoTotal: number
pesoQuadradoTotal: number
proporcao: number
amostraEfetiva: number
evidenciasSemData: number
dataCorte: string
}
export interface AutoriaAvaliada {
processo: Registro
tramitacao: Tramitacao
}
/** Cada autoria entra uma vez, no maior estágio; as contagens são exclusivas. */
export function resumirAutorias(autorias: AutoriaAvaliada[], corte: string) {
const resumo: ResumoTramitacao = {
normativas: 0,
porEtapa: { sem_avanco: 0, instrucao: 0, aprovado_senado: 0, camara: 0, sancao: 0, norma: 0 },
pontosPonderados: 0, pesoTotal: 0, pesoQuadradoTotal: 0, proporcao: 0, amostraEfetiva: 0,
evidenciasSemData: 0, dataCorte: corte,
}
let produtividadePonderada = 0
for (const { processo, tramitacao: t } of autorias) {
const peso = PESO_PROPOSICAO[extrairSigla(processo)] ?? 0.5
produtividadePonderada += peso
if (!t.normativa) continue
resumo.normativas++
resumo.porEtapa[t.etapa]++
resumo.pontosPonderados += t.pontos * peso
resumo.pesoTotal += peso
resumo.pesoQuadradoTotal += peso * peso
resumo.evidenciasSemData += t.evidenciasSemData
}
resumo.proporcao = resumo.pesoTotal > 0 ? resumo.pontosPonderados / (4 * resumo.pesoTotal) : 0
resumo.amostraEfetiva = resumo.pesoQuadradoTotal > 0 ? resumo.pesoTotal ** 2 / resumo.pesoQuadradoTotal : 0
return {
autoriasTotal: autorias.length,
autoriasAprovadas: resumo.porEtapa.aprovado_senado + resumo.porEtapa.camara + resumo.porEtapa.sancao + resumo.porEtapa.norma,
produtividadePonderada,
efetividadePonderada: resumo.pontosPonderados,
efetividadeBase: 4 * resumo.pesoTotal,
tramitacao: resumo,
}
}
/** Regularização descritiva: K propostas efetivas, não meses nem peso de PECs. */
export function regularizarEfetividade(r: ResumoTramitacao, media: number, k = 12): number {
if (r.normativas === 0) return 0
return (r.proporcao * r.amostraEfetiva + media * k) / (r.amostraEfetiva + k)
}
/** Percentil empírico com empates no ponto médio; não pressupõe normalidade. */
export function percentisEfetividade(valores: number[], referencia: boolean[], possuiPropostas: boolean[]): number[] {
const base = valores.filter((_, i) => referencia[i] && possuiPropostas[i])
if (base.length === 0 || base.every((v) => v === 0)) return valores.map(() => 0)
return valores.map((v, i) => {
if (!possuiPropostas[i]) return 0
const menores = base.filter((x) => x < v - 1e-12).length
const iguais = base.filter((x) => Math.abs(x - v) <= 1e-12).length
return Math.round(1000 * (menores + iguais / 2) / base.length) / 10
})
}