freoa-verifica 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freoa_verifica/__init__.py +5 -0
- freoa_verifica/acervo.py +161 -0
- freoa_verifica/aliases.py +150 -0
- freoa_verifica/authority.py +213 -0
- freoa_verifica/bateria.py +333 -0
- freoa_verifica/bench.py +275 -0
- freoa_verifica/captura.py +402 -0
- freoa_verifica/citation.py +1253 -0
- freoa_verifica/cli.py +423 -0
- freoa_verifica/connectors/__init__.py +21 -0
- freoa_verifica/connectors/base.py +213 -0
- freoa_verifica/connectors/bcb.py +296 -0
- freoa_verifica/connectors/challenge.py +196 -0
- freoa_verifica/connectors/cvm_colegiado.py +427 -0
- freoa_verifica/connectors/cvm_normas.py +618 -0
- freoa_verifica/connectors/datajud.py +283 -0
- freoa_verifica/connectors/guarda.py +159 -0
- freoa_verifica/connectors/httputil.py +219 -0
- freoa_verifica/connectors/planalto.py +316 -0
- freoa_verifica/connectors/planalto_urls.json +148 -0
- freoa_verifica/connectors/senado.py +339 -0
- freoa_verifica/connectors/stf.py +430 -0
- freoa_verifica/connectors/stj.py +811 -0
- freoa_verifica/connectors/tcu.py +311 -0
- freoa_verifica/connectors/tjdft.py +402 -0
- freoa_verifica/connectors/tjsp.py +353 -0
- freoa_verifica/contrato.py +23 -0
- freoa_verifica/courts.py +22 -0
- freoa_verifica/data/corpus_semente.json +79 -0
- freoa_verifica/data/locucoes-juridicas-1.json +16 -0
- freoa_verifica/data/snapshots-oficiais.json +20 -0
- freoa_verifica/data/tesauro-1.json +26 -0
- freoa_verifica/data/timbres-cvm.json +8 -0
- freoa_verifica/derivados.py +319 -0
- freoa_verifica/fronteira.py +81 -0
- freoa_verifica/identidade_http.py +38 -0
- freoa_verifica/identity.py +91 -0
- freoa_verifica/ids.py +25 -0
- freoa_verifica/limites.py +73 -0
- freoa_verifica/mcp_server.py +458 -0
- freoa_verifica/parsers/__init__.py +27 -0
- freoa_verifica/parsers/bcb_json.py +90 -0
- freoa_verifica/parsers/cvm_docx.py +762 -0
- freoa_verifica/parsers/html_devices.py +1285 -0
- freoa_verifica/parsers/isolated.py +269 -0
- freoa_verifica/parsers/model.py +195 -0
- freoa_verifica/parsers/paths.py +392 -0
- freoa_verifica/parsers/pdf_dispositivos.py +279 -0
- freoa_verifica/parsers/planalto.py +89 -0
- freoa_verifica/parsers/sentinel.py +269 -0
- freoa_verifica/registro.py +244 -0
- freoa_verifica/rest.py +337 -0
- freoa_verifica/rotulos.py +42 -0
- freoa_verifica/sanitizacao.py +113 -0
- freoa_verifica/search.py +146 -0
- freoa_verifica/semente.py +1070 -0
- freoa_verifica/service.py +3371 -0
- freoa_verifica/situacao.py +459 -0
- freoa_verifica/snapshot.py +1151 -0
- freoa_verifica/store/__init__.py +6 -0
- freoa_verifica/store/blobs.py +98 -0
- freoa_verifica/store/db.py +142 -0
- freoa_verifica/store/migrations/0001_inicial.sql +251 -0
- freoa_verifica/store/migrations/0002_parsed_document.sql +19 -0
- freoa_verifica/store/migrations/0003_sync_state.sql +15 -0
- freoa_verifica/store/migrations/0004_parse_identity.sql +14 -0
- freoa_verifica/store/migrations/005_work_year.sql +6 -0
- freoa_verifica/store/migrations/006_rubrica_fts.sql +14 -0
- freoa_verifica/store/migrations/007_work_status.sql +11 -0
- freoa_verifica/store/migrations/008_fts_staging.sql +7 -0
- freoa_verifica/store/migrations/009_meta.sql +5 -0
- freoa_verifica/store/migrations/010_candidato_ao_vivo.sql +4 -0
- freoa_verifica/store/migrations/011_candidato_retroativo.sql +6 -0
- freoa_verifica/store/migrations/012_candidato_por_documento.py +27 -0
- freoa_verifica/store/migrations/013_origem_nao_publicada.py +7 -0
- freoa_verifica/store/migrations/014_interpretacao_da_captura.py +15 -0
- freoa_verifica/store/migrations/015_interpretacao_legada.py +15 -0
- freoa_verifica/store/migrations/016_conferencia_legada.py +11 -0
- freoa_verifica/store/migrations/__init__.py +1 -0
- freoa_verifica/store/repo.py +817 -0
- freoa_verifica/temporal.py +649 -0
- freoa_verifica/textcmp.py +253 -0
- freoa_verifica/texto_resposta.py +578 -0
- freoa_verifica/transport.py +1459 -0
- freoa_verifica/trava.py +130 -0
- freoa_verifica/verify.py +809 -0
- freoa_verifica-1.3.0.dist-info/METADATA +153 -0
- freoa_verifica-1.3.0.dist-info/RECORD +92 -0
- freoa_verifica-1.3.0.dist-info/WHEEL +5 -0
- freoa_verifica-1.3.0.dist-info/entry_points.txt +3 -0
- freoa_verifica-1.3.0.dist-info/licenses/LICENSE +662 -0
- freoa_verifica-1.3.0.dist-info/top_level.txt +1 -0
freoa_verifica/acervo.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Acervo efetivo (contrato C7): manifesto do snapshot x conteúdo do home."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
MANIFESTO_LOCAL = "acervo-manifesto.json"
|
|
13
|
+
# Mínimo embutido na versão, usado quando nenhum snapshot foi instalado.
|
|
14
|
+
# shortcut: valor-guarda (home vazio alerta); fixar com os totais do snapshot
|
|
15
|
+
# publicado a cada release.
|
|
16
|
+
MINIMO_EMBUTIDO = {"obras": 1, "decisoes": 0}
|
|
17
|
+
|
|
18
|
+
_OBRAS_SQL = "SELECT authority||':'||kind||':'||number||'/'||year FROM work"
|
|
19
|
+
_DECISOES_SQL = (
|
|
20
|
+
"SELECT court||'|'||COALESCE(class,'')||'|'||COALESCE(number,'')||'|'||"
|
|
21
|
+
"COALESCE(judgment_date,'')||'|'||kind FROM decision"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ids(conn: sqlite3.Connection, sql: str) -> list[str]:
|
|
26
|
+
return sorted(r[0] for r in conn.execute(sql).fetchall())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _digest(ids: list[str]) -> str:
|
|
30
|
+
return hashlib.sha256("\n".join(ids).encode("utf-8")).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def derived_version(conn: sqlite3.Connection) -> str | None:
|
|
34
|
+
try:
|
|
35
|
+
row = conn.execute("SELECT value FROM meta WHERE key='derived_version'").fetchone()
|
|
36
|
+
except sqlite3.Error:
|
|
37
|
+
return None
|
|
38
|
+
return str(row[0]) if row else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def manifest_fields(conn: sqlite3.Connection, db_sha256: str | None = None) -> dict[str, Any]:
|
|
42
|
+
"""Campos do manifesto publicados pelo snapshot (obras, decisões, derivados, hashes)."""
|
|
43
|
+
obras, decisoes = _ids(conn, _OBRAS_SQL), _ids(conn, _DECISOES_SQL)
|
|
44
|
+
return {
|
|
45
|
+
"obras": len(obras),
|
|
46
|
+
"decisoes": len(decisoes),
|
|
47
|
+
"derived_version": derived_version(conn),
|
|
48
|
+
"obras_ids": obras,
|
|
49
|
+
"obras_ids_sha256": _digest(obras),
|
|
50
|
+
"decisoes_ids_sha256": _digest(decisoes),
|
|
51
|
+
"db_sha256": db_sha256,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_installed(home: Path, manifest: dict[str, Any]) -> None:
|
|
56
|
+
keys = ("obras", "decisoes", "derived_version", "obras_ids", "obras_ids_sha256",
|
|
57
|
+
"decisoes_ids_sha256", "db_sha256", "vinculado", "snapshot_sha256", "snapshot_derived_version",
|
|
58
|
+
"vinculo_motivo")
|
|
59
|
+
(Path(home) / MANIFESTO_LOCAL).write_text(
|
|
60
|
+
json.dumps({k: manifest.get(k) for k in keys}, ensure_ascii=False, sort_keys=True),
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def status(conn: sqlite3.Connection, home: Path) -> dict[str, Any] | None:
|
|
66
|
+
"""None se o acervo atende ao manifesto instalado (ou ao mínimo); senão o aviso."""
|
|
67
|
+
path = Path(home) / MANIFESTO_LOCAL
|
|
68
|
+
try:
|
|
69
|
+
ref = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
|
|
70
|
+
except (OSError, ValueError):
|
|
71
|
+
ref = None
|
|
72
|
+
origem = "snapshot_instalado" if ref else "minimo_embutido"
|
|
73
|
+
ref = ref or MINIMO_EMBUTIDO
|
|
74
|
+
obras, decisoes = _ids(conn, _OBRAS_SQL), _ids(conn, _DECISOES_SQL)
|
|
75
|
+
motivos: list[str] = []
|
|
76
|
+
if len(obras) < int(ref.get("obras") or 0):
|
|
77
|
+
motivos.append("obras_abaixo_do_manifesto")
|
|
78
|
+
if len(decisoes) < int(ref.get("decisoes") or 0):
|
|
79
|
+
motivos.append("decisoes_abaixo_do_manifesto")
|
|
80
|
+
if ref.get("obras_ids") is not None:
|
|
81
|
+
if set(ref["obras_ids"]) - set(obras):
|
|
82
|
+
motivos.append("obras_ausentes_do_manifesto")
|
|
83
|
+
elif ref.get("obras_ids_sha256") and len(obras) == ref.get("obras") and _digest(obras) != ref["obras_ids_sha256"]:
|
|
84
|
+
motivos.append("obras_ids_divergentes")
|
|
85
|
+
if (ref.get("decisoes_ids_sha256") and len(decisoes) == ref.get("decisoes")
|
|
86
|
+
and _digest(decisoes) != ref["decisoes_ids_sha256"]):
|
|
87
|
+
motivos.append("decisoes_ids_divergentes")
|
|
88
|
+
dv = ref.get("derived_version")
|
|
89
|
+
if dv is not None and derived_version(conn) != str(dv):
|
|
90
|
+
motivos.append("derived_version_divergente")
|
|
91
|
+
from freoa_verifica.derivados import compativel
|
|
92
|
+
|
|
93
|
+
if not compativel(conn):
|
|
94
|
+
motivos.append("derivados_desatualizados")
|
|
95
|
+
from freoa_verifica.derivados import REASON_NAO_PUBLICADA, origem_nao_publicada
|
|
96
|
+
|
|
97
|
+
if origem_nao_publicada(conn):
|
|
98
|
+
motivos.append(REASON_NAO_PUBLICADA)
|
|
99
|
+
if not motivos:
|
|
100
|
+
return None
|
|
101
|
+
return {
|
|
102
|
+
"codigo": "acervo_incompleto",
|
|
103
|
+
"referencia": origem,
|
|
104
|
+
"motivos": motivos,
|
|
105
|
+
"obras": len(obras),
|
|
106
|
+
"decisoes": len(decisoes),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def verificar_digest_instalado(provenance: dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
"""Compara o digest do snapshot instalado com o da lista embutida para esta versão do pacote."""
|
|
112
|
+
from freoa_verifica import __version__
|
|
113
|
+
from freoa_verifica.snapshot import _da_versao_instalada, _snapshots_oficiais
|
|
114
|
+
|
|
115
|
+
digest = provenance.get("snapshot_sha256")
|
|
116
|
+
esperados = [e["sha256"] for e in _snapshots_oficiais() if _da_versao_instalada(e)]
|
|
117
|
+
detalhe = {"versao": __version__, "instalado": digest, "esperados": esperados}
|
|
118
|
+
if not digest:
|
|
119
|
+
return {"nome": "snapshot_digest", "status": "alerta", "detalhe": {**detalhe, "motivo": "sem snapshot instalado"}}
|
|
120
|
+
ok = digest in esperados
|
|
121
|
+
return {"nome": "snapshot_digest", "status": "ok" if ok else "alerta",
|
|
122
|
+
"detalhe": {**detalhe, "confere": ok}}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def attestation_instalacao(provenance: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
126
|
+
"""Registro do instalador (instalacao.json) confrontado com esta instalação.
|
|
127
|
+
|
|
128
|
+
`ok` exige instalação concluída, versão igual à do pacote, attestation verificada e, havendo snapshot
|
|
129
|
+
instalado, o mesmo digest gravado pelo instalador. Ausente ou divergente é `alerta`.
|
|
130
|
+
"""
|
|
131
|
+
from freoa_verifica import __version__
|
|
132
|
+
|
|
133
|
+
base = os.environ.get("LOCALAPPDATA") or str(Path.home() / ".local" / "share")
|
|
134
|
+
try:
|
|
135
|
+
data = json.loads((Path(base) / "freoa-verifica" / "instalacao.json").read_text(encoding="utf-8-sig"))
|
|
136
|
+
if not isinstance(data, dict):
|
|
137
|
+
raise ValueError
|
|
138
|
+
except (OSError, ValueError):
|
|
139
|
+
return {"nome": "attestation", "status": "alerta",
|
|
140
|
+
"detalhe": {"attestation_verificada": None, "motivos": ["sem_registro_do_instalador"]}}
|
|
141
|
+
motivos = []
|
|
142
|
+
if data.get("concluida") is not True:
|
|
143
|
+
motivos.append("instalacao_nao_concluida")
|
|
144
|
+
if data.get("versao") != __version__:
|
|
145
|
+
motivos.append("versao_divergente")
|
|
146
|
+
if data.get("attestation_verificada") is not True:
|
|
147
|
+
motivos.append("attestation_nao_verificada")
|
|
148
|
+
instalado = (provenance or {}).get("snapshot_sha256")
|
|
149
|
+
if provenance is not None and instalado and data.get("digest_snapshot") != instalado:
|
|
150
|
+
motivos.append("digest_snapshot_divergente")
|
|
151
|
+
detalhe = {k: data.get(k) for k in ("attestation_verificada", "versao", "concluida", "sha256_wheel", "digest_snapshot")}
|
|
152
|
+
return {"nome": "attestation", "status": "alerta" if motivos else "ok",
|
|
153
|
+
"detalhe": {**detalhe, "versao_instalada": __version__, "motivos": motivos}}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def installed_provenance(home: Path) -> dict[str, Any]:
|
|
157
|
+
try:
|
|
158
|
+
data = json.loads((Path(home) / MANIFESTO_LOCAL).read_text(encoding="utf-8"))
|
|
159
|
+
return {k: data.get(k) for k in ("vinculado", "snapshot_sha256", "snapshot_derived_version", "vinculo_motivo")}
|
|
160
|
+
except (OSError, ValueError, AttributeError):
|
|
161
|
+
return {"vinculado": False}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Apelidos estáveis de obras normativas brasileiras → WorkKey."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class WorkKey:
|
|
10
|
+
"""Chave de obra normativa (autoridade + espécie + número + ano)."""
|
|
11
|
+
|
|
12
|
+
authority: str # federal|cvm|cmn|bcb|stf|stj
|
|
13
|
+
kind: str
|
|
14
|
+
number: str
|
|
15
|
+
year: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Apelidos canônicos (chave = fold_alias; valor = WorkKey).
|
|
19
|
+
_LSA = WorkKey("federal", "lei", "6404", 1976)
|
|
20
|
+
_CVM_LEI = WorkKey("federal", "lei", "6385", 1976)
|
|
21
|
+
_CC = WorkKey("federal", "lei", "10406", 2002)
|
|
22
|
+
_CPC = WorkKey("federal", "lei", "13105", 2015)
|
|
23
|
+
_CDC = WorkKey("federal", "lei", "8078", 1990)
|
|
24
|
+
_LRF_FALENCIAS = WorkKey("federal", "lei", "11101", 2005)
|
|
25
|
+
_LRF_RESPONSABILIDADE = WorkKey("federal", "lei_complementar", "101", 2000)
|
|
26
|
+
_CF = WorkKey("federal", "constituicao", "1988", 1988)
|
|
27
|
+
|
|
28
|
+
# LRF é ambíguo: Lei 11.101/2005 (falências) vs LC 101/2000 (responsabilidade fiscal).
|
|
29
|
+
LRF_CANDIDATES: tuple[WorkKey, WorkKey] = (_LRF_FALENCIAS, _LRF_RESPONSABILIDADE)
|
|
30
|
+
|
|
31
|
+
# Apelidos curtos que exigem contexto normativo (art., §, inciso, do/da).
|
|
32
|
+
CONTEXT_REQUIRED_ALIASES: frozenset[str] = frozenset({"cc", "cf"})
|
|
33
|
+
|
|
34
|
+
ALIASES: dict[str, WorkKey] = {
|
|
35
|
+
"lei das s.a.": _LSA,
|
|
36
|
+
"lei das s.a": _LSA,
|
|
37
|
+
"lei das sa": _LSA,
|
|
38
|
+
"lsa": _LSA,
|
|
39
|
+
"lei das sociedades por acoes": _LSA,
|
|
40
|
+
"lei do mercado de capitais": _CVM_LEI,
|
|
41
|
+
"lei da cvm": _CVM_LEI,
|
|
42
|
+
"codigo civil": _CC,
|
|
43
|
+
"cc": _CC,
|
|
44
|
+
"cpc": _CPC,
|
|
45
|
+
"codigo de processo civil": _CPC,
|
|
46
|
+
"cdc": _CDC,
|
|
47
|
+
"codigo de defesa do consumidor": _CDC,
|
|
48
|
+
"lei de falencias": _LRF_FALENCIAS,
|
|
49
|
+
# "lrf" tratado à parte (ambíguo) — não entra em ALIASES unívoco
|
|
50
|
+
"lei anticorrupcao": WorkKey("federal", "lei", "12846", 2013),
|
|
51
|
+
"lgpd": WorkKey("federal", "lei", "13709", 2018),
|
|
52
|
+
"marco da securitizacao": WorkKey("federal", "lei", "14430", 2022),
|
|
53
|
+
"lei de arbitragem": WorkKey("federal", "lei", "9307", 1996),
|
|
54
|
+
"constituicao": _CF,
|
|
55
|
+
"cf": _CF,
|
|
56
|
+
"cf/88": _CF,
|
|
57
|
+
"adct": _CF,
|
|
58
|
+
"ato das disposicoes constitucionais transitorias": _CF,
|
|
59
|
+
"constituicao federal": _CF,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Formas de superfície (com e sem acento) para regex de extração.
|
|
63
|
+
ALIAS_SURFACES: list[tuple[str, WorkKey]] = [
|
|
64
|
+
("Lei das Sociedades por Ações", _LSA),
|
|
65
|
+
("Lei das Sociedades por Acoes", _LSA),
|
|
66
|
+
("Lei das S.A.", _LSA),
|
|
67
|
+
("Lei das S.A", _LSA),
|
|
68
|
+
("LSA", _LSA),
|
|
69
|
+
("Lei do Mercado de Capitais", _CVM_LEI),
|
|
70
|
+
("Lei da CVM", _CVM_LEI),
|
|
71
|
+
("Código Civil", _CC),
|
|
72
|
+
("Codigo Civil", _CC),
|
|
73
|
+
("CC", _CC),
|
|
74
|
+
("Código de Processo Civil", _CPC),
|
|
75
|
+
("Codigo de Processo Civil", _CPC),
|
|
76
|
+
("CPC", _CPC),
|
|
77
|
+
("Código de Defesa do Consumidor", _CDC),
|
|
78
|
+
("Codigo de Defesa do Consumidor", _CDC),
|
|
79
|
+
("CDC", _CDC),
|
|
80
|
+
("Lei de Falências", _LRF_FALENCIAS),
|
|
81
|
+
("Lei de Falencias", _LRF_FALENCIAS),
|
|
82
|
+
("Lei de Recuperação e Falências", _LRF_FALENCIAS),
|
|
83
|
+
("Lei de Recuperacao e Falencias", _LRF_FALENCIAS),
|
|
84
|
+
("Lei de Recuperação Judicial e Falências", _LRF_FALENCIAS),
|
|
85
|
+
("Lei de Recuperacao Judicial e Falencias", _LRF_FALENCIAS),
|
|
86
|
+
("Lei de Recuperação de Empresas e Falências", _LRF_FALENCIAS),
|
|
87
|
+
("LRF", _LRF_FALENCIAS), # ambiguidade resolvida em citation.extract
|
|
88
|
+
("Lei Anticorrupção", WorkKey("federal", "lei", "12846", 2013)),
|
|
89
|
+
("Lei Anticorrupcao", WorkKey("federal", "lei", "12846", 2013)),
|
|
90
|
+
("LGPD", WorkKey("federal", "lei", "13709", 2018)),
|
|
91
|
+
("Marco da Securitização", WorkKey("federal", "lei", "14430", 2022)),
|
|
92
|
+
("Marco da Securitizacao", WorkKey("federal", "lei", "14430", 2022)),
|
|
93
|
+
("Lei de Arbitragem", WorkKey("federal", "lei", "9307", 1996)),
|
|
94
|
+
("Constituição Federal", _CF),
|
|
95
|
+
("Constituicao Federal", _CF),
|
|
96
|
+
("Constituição", _CF),
|
|
97
|
+
("Constituicao", _CF),
|
|
98
|
+
("Ato das Disposições Constitucionais Transitórias", _CF),
|
|
99
|
+
("Ato das Disposicoes Constitucionais Transitorias", _CF),
|
|
100
|
+
("ADCT", _CF),
|
|
101
|
+
("CF/88", _CF),
|
|
102
|
+
("CF", _CF),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def fold_alias(text: str) -> str:
|
|
107
|
+
"""Normaliza texto para busca em ALIASES (minúsculas, sem diacríticos)."""
|
|
108
|
+
import unicodedata
|
|
109
|
+
|
|
110
|
+
nfkd = unicodedata.normalize("NFKD", text)
|
|
111
|
+
bare = "".join(c for c in nfkd if not unicodedata.combining(c))
|
|
112
|
+
return " ".join(bare.lower().split())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def lookup_alias(text: str) -> WorkKey | None:
|
|
116
|
+
key = fold_alias(text)
|
|
117
|
+
if key == "lrf":
|
|
118
|
+
return None # ambíguo — ver lookup_lrf / citation
|
|
119
|
+
return ALIASES.get(key)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def lookup_lrf(text: str, surrounding: str | None = None) -> list[WorkKey]:
|
|
123
|
+
"""Resolve LRF: se o entorno explicita a lei, devolve só ela; senão ambas."""
|
|
124
|
+
ctx = surrounding or text
|
|
125
|
+
folded = fold_alias(ctx)
|
|
126
|
+
# LC 101 / Lei Complementar 101 / responsabilidade fiscal
|
|
127
|
+
if re_search_lc101(folded) or "responsabilidade fiscal" in folded:
|
|
128
|
+
return [_LRF_RESPONSABILIDADE]
|
|
129
|
+
# Lei 11.101 / falências
|
|
130
|
+
if (
|
|
131
|
+
"11.101" in ctx
|
|
132
|
+
or "11101" in folded.replace(".", "")
|
|
133
|
+
or "falenc" in folded
|
|
134
|
+
or "recuperacao judicial" in folded
|
|
135
|
+
or "recuperação judicial" in fold_alias(ctx)
|
|
136
|
+
):
|
|
137
|
+
return [_LRF_FALENCIAS]
|
|
138
|
+
return list(LRF_CANDIDATES)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def re_search_lc101(folded: str) -> bool:
|
|
142
|
+
import re
|
|
143
|
+
|
|
144
|
+
return bool(
|
|
145
|
+
re.search(
|
|
146
|
+
r"\b(?:lc|lei\s+complementar)\s*(?:n[º°o.]?\s*)?101\b",
|
|
147
|
+
folded,
|
|
148
|
+
re.I,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Atributos verificáveis de autoridade e rótulo A–E (regra pública)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
RULES_VERSION = "autoridade-1"
|
|
9
|
+
|
|
10
|
+
# Espécies reconhecidas (forma canônica).
|
|
11
|
+
SPECIES = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"sumula_vinculante",
|
|
14
|
+
"controle_concentrado",
|
|
15
|
+
"repercussao_geral",
|
|
16
|
+
"repetitivo",
|
|
17
|
+
"irdr_iac",
|
|
18
|
+
"sumula",
|
|
19
|
+
"orgao_cupula",
|
|
20
|
+
"acordao",
|
|
21
|
+
"monocratica",
|
|
22
|
+
"administrativa",
|
|
23
|
+
"informativo",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Órgãos de cúpula / colegiados superiores (rótulo C) — só judiciais.
|
|
28
|
+
CUPULA_ORGANS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"pleno",
|
|
31
|
+
"plenario",
|
|
32
|
+
"tribunal pleno",
|
|
33
|
+
"corte especial",
|
|
34
|
+
"secao",
|
|
35
|
+
"primeira secao",
|
|
36
|
+
"segunda secao",
|
|
37
|
+
"terceira secao",
|
|
38
|
+
"orgao especial",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
CONTROLE_CLASSES = frozenset({"adi", "adc", "adpf", "ado"})
|
|
43
|
+
|
|
44
|
+
# Cortes administrativas: nunca promovidas por órgão de cúpula.
|
|
45
|
+
_ADMIN_COURTS = frozenset({"CVM", "TCU", "BCB", "CMN"})
|
|
46
|
+
|
|
47
|
+
_SPECIES_ALIASES: dict[str, str] = {
|
|
48
|
+
"acordao": "acordao",
|
|
49
|
+
"acordaos": "acordao",
|
|
50
|
+
"monocratica": "monocratica",
|
|
51
|
+
"decisao monocratica": "monocratica",
|
|
52
|
+
"decisoes monocraticas": "monocratica",
|
|
53
|
+
"administrativa": "administrativa",
|
|
54
|
+
"controle concentrado": "controle_concentrado",
|
|
55
|
+
"controle_concentrado": "controle_concentrado",
|
|
56
|
+
"repercussao geral": "repercussao_geral",
|
|
57
|
+
"repercussao_geral": "repercussao_geral",
|
|
58
|
+
"repetitivo": "repetitivo",
|
|
59
|
+
"tema repetitivo": "repetitivo",
|
|
60
|
+
"irdr": "irdr_iac",
|
|
61
|
+
"iac": "irdr_iac",
|
|
62
|
+
"irdr_iac": "irdr_iac",
|
|
63
|
+
"irdr/iac": "irdr_iac",
|
|
64
|
+
"sumula": "sumula",
|
|
65
|
+
"sumula vinculante": "sumula_vinculante",
|
|
66
|
+
"sumula_vinculante": "sumula_vinculante",
|
|
67
|
+
"orgao_cupula": "orgao_cupula",
|
|
68
|
+
"informativo": "informativo",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class AuthorityAttributes:
|
|
74
|
+
species: str
|
|
75
|
+
court: str
|
|
76
|
+
organ: str | None = None
|
|
77
|
+
class_name: str | None = None
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if self.species not in SPECIES:
|
|
81
|
+
raise ValueError(f"espécie desconhecida: {self.species}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class AuthorityLabel:
|
|
86
|
+
label: str # A..E
|
|
87
|
+
rule: str
|
|
88
|
+
attributes: AuthorityAttributes
|
|
89
|
+
rules_version: str = RULES_VERSION
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def normalize_species(species: str) -> str:
|
|
93
|
+
"""Normaliza espécie (acórdão/ACÓRDÃO/Acordao → acordao)."""
|
|
94
|
+
key = _norm(species)
|
|
95
|
+
if key in _SPECIES_ALIASES:
|
|
96
|
+
return _SPECIES_ALIASES[key]
|
|
97
|
+
# remove underscores duplicados / espaços já tratados por _norm
|
|
98
|
+
compact = key.replace(" ", "_")
|
|
99
|
+
if compact in SPECIES:
|
|
100
|
+
return compact
|
|
101
|
+
if compact in _SPECIES_ALIASES:
|
|
102
|
+
return _SPECIES_ALIASES[compact]
|
|
103
|
+
raise ValueError(f"espécie desconhecida: {species}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def classify_authority(
|
|
107
|
+
*,
|
|
108
|
+
species: str,
|
|
109
|
+
court: str,
|
|
110
|
+
organ: str | None = None,
|
|
111
|
+
class_name: str | None = None,
|
|
112
|
+
) -> AuthorityLabel:
|
|
113
|
+
"""Deriva rótulo A–E e devolve sempre atributos + regra aplicada."""
|
|
114
|
+
species_n = normalize_species(species)
|
|
115
|
+
# Preserva grafia comprovada do órgão (ex.: Plenário), só normaliza internamente.
|
|
116
|
+
attrs = AuthorityAttributes(
|
|
117
|
+
species=species_n,
|
|
118
|
+
court=(court or "").strip().upper(),
|
|
119
|
+
organ=organ,
|
|
120
|
+
class_name=class_name,
|
|
121
|
+
)
|
|
122
|
+
label, rule = _derive(attrs)
|
|
123
|
+
return AuthorityLabel(label=label, rule=rule, attributes=attrs)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _derive(attrs: AuthorityAttributes) -> tuple[str, str]:
|
|
127
|
+
species = attrs.species
|
|
128
|
+
court = attrs.court
|
|
129
|
+
organ_norm = _norm(attrs.organ)
|
|
130
|
+
class_norm = _norm(attrs.class_name)
|
|
131
|
+
|
|
132
|
+
# Exclusões antes de qualquer promoção (monocrática / administrativa).
|
|
133
|
+
if species == "monocratica":
|
|
134
|
+
return "E", "especie=monocratica"
|
|
135
|
+
if species == "administrativa" or court in _ADMIN_COURTS:
|
|
136
|
+
return "E", "administrativa_ou_corte_administrativa"
|
|
137
|
+
|
|
138
|
+
# A — súmula vinculante; ADI/ADC/ADPF colegiados; súmulas STF/STJ
|
|
139
|
+
if species == "sumula_vinculante":
|
|
140
|
+
return "A", "especie=sumula_vinculante"
|
|
141
|
+
if species == "controle_concentrado" or class_norm in CONTROLE_CLASSES:
|
|
142
|
+
return "A", "controle_concentrado_ou_classe_adi_adc_adpf"
|
|
143
|
+
if species == "sumula" and court in {"STF", "STJ"}:
|
|
144
|
+
return "A", f"sumula_{court.lower()}"
|
|
145
|
+
|
|
146
|
+
# B — tema RG, repetitivo, IRDR/IAC
|
|
147
|
+
if species == "repercussao_geral":
|
|
148
|
+
return "B", "especie=repercussao_geral"
|
|
149
|
+
if species == "repetitivo":
|
|
150
|
+
return "B", "especie=repetitivo"
|
|
151
|
+
if species == "irdr_iac":
|
|
152
|
+
return "B", "especie=irdr_iac"
|
|
153
|
+
|
|
154
|
+
# C — Pleno / Corte Especial / Seções / Órgão Especial (judiciais)
|
|
155
|
+
if species == "orgao_cupula" or _is_cupula(organ_norm):
|
|
156
|
+
return "C", "orgao_cupula_ou_colegiado_superior"
|
|
157
|
+
|
|
158
|
+
# D — acórdão de turma/câmara (padrão residual de julgamento colegiado)
|
|
159
|
+
if species in {"acordao", "informativo"}:
|
|
160
|
+
return "D", "acordao_turma_camara_ou_equivalente"
|
|
161
|
+
|
|
162
|
+
return "E", "residual_especie_" + species
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _norm(value: str | None) -> str:
|
|
166
|
+
if not value:
|
|
167
|
+
return ""
|
|
168
|
+
return (
|
|
169
|
+
value.strip()
|
|
170
|
+
.lower()
|
|
171
|
+
.replace("á", "a")
|
|
172
|
+
.replace("ã", "a")
|
|
173
|
+
.replace("à", "a")
|
|
174
|
+
.replace("é", "e")
|
|
175
|
+
.replace("ê", "e")
|
|
176
|
+
.replace("í", "i")
|
|
177
|
+
.replace("ó", "o")
|
|
178
|
+
.replace("ô", "o")
|
|
179
|
+
.replace("õ", "o")
|
|
180
|
+
.replace("ú", "u")
|
|
181
|
+
.replace("ç", "c")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _is_cupula(organ_norm: str) -> bool:
|
|
186
|
+
if not organ_norm:
|
|
187
|
+
return False
|
|
188
|
+
if organ_norm in CUPULA_ORGANS:
|
|
189
|
+
return True
|
|
190
|
+
return any(
|
|
191
|
+
token in organ_norm
|
|
192
|
+
for token in (
|
|
193
|
+
"pleno",
|
|
194
|
+
"plenario",
|
|
195
|
+
"corte especial",
|
|
196
|
+
"orgao especial",
|
|
197
|
+
"secao",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def as_dict(label: AuthorityLabel) -> dict[str, Any]:
|
|
203
|
+
return {
|
|
204
|
+
"label": label.label,
|
|
205
|
+
"rule": label.rule,
|
|
206
|
+
"rules_version": label.rules_version,
|
|
207
|
+
"attributes": {
|
|
208
|
+
"species": label.attributes.species,
|
|
209
|
+
"court": label.attributes.court,
|
|
210
|
+
"organ": label.attributes.organ,
|
|
211
|
+
"class_name": label.attributes.class_name,
|
|
212
|
+
},
|
|
213
|
+
}
|