Iniciativa Cidadã Independente · Transparência Parlamentar
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
  })
}