Metadata-Version: 2.5
Name: jurisgrow
Version: 0.1.0.dev0
Summary: Expansão contínua de classes para classificadores XGBoost de documentos jurídicos
Project-URL: Homepage, https://github.com/jurisgrow/jurisgrow
Author: JurisGrow
License: MIT
Keywords: class-incremental,continual-learning,legal-nlp,open-set,xgboost
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Requires-Dist: joblib>=1.3
Requires-Dist: numpy>=1.24
Requires-Dist: pydantic>=2.0
Requires-Dist: scikit-learn>=1.6
Requires-Dist: scipy>=1.10
Requires-Dist: xgboost>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pandas>=2.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: discovery
Requires-Dist: hdbscan>=0.8; extra == 'discovery'
Requires-Dist: sentence-transformers>=2.2; extra == 'discovery'
Provides-Extra: explain
Requires-Dist: shap>=0.44; extra == 'explain'
Provides-Extra: notebooks
Requires-Dist: jupyter>=1.0; extra == 'notebooks'
Requires-Dist: matplotlib>=3.7; extra == 'notebooks'
Requires-Dist: nbclient>=0.9; extra == 'notebooks'
Requires-Dist: nbformat>=5.9; extra == 'notebooks'
Description-Content-Type: text/markdown

# JurisGrow

Expansão contínua de classes para classificadores de documentos jurídicos.

O JurisGrow envolve um **XGBoost já treinado** e permite acrescentar classes novas
ao longo do tempo — sem retreinar o modelo original e medindo, a cada passo,
quanto as classes antigas sofreram.

```bash
pip install jurisgrow
```

> O JurisGrow não realiza nenhuma comunicação de rede por padrão: sem telemetria,
> sem analytics, sem chamadas a serviços externos, sem dependência de LLM.

---

## O que é

Um classificador em produção é um ativo: foi validado, tem métricas conhecidas e
alguém confia nele. Quando aparece um tipo de documento novo, a saída óbvia —
retreinar tudo — custa caro, exige o dataset histórico completo e coloca em risco
tudo o que já funcionava.

O JurisGrow é a alternativa. O modelo base vira **conhecimento histórico
congelado**; cada classe nova ganha um **residual** pequeno que compete com ele na
inferência. Replay dá memória, mineração de negativos duros dá precisão de
fronteira, e a política de aceitação recusa qualquer residual que danifique as
classes antigas.

```
        BASE (congelado)          estabilidade
              +
        RESIDUAIS                 plasticidade
              +
        REPLAY                    memória
              +
        NEGATIVOS DUROS           fronteira
              +
        DETECÇÃO DE UNKNOWN       mundo aberto
```

## Por que não simplesmente retreinar o XGBoost?

| | Retreino completo | JurisGrow |
|---|---|---|
| Dataset histórico | necessário por inteiro | só o replay compacto |
| Custo por classe nova | treino completo | um residual pequeno |
| Risco às classes antigas | não medido por construção | **medido e vetado** — leia o `≥` |
| Modelo validado | substituído | preservado |

Retreinar continua sendo uma opção legítima — e às vezes a melhor. O JurisGrow
existe para quando ela é cara, arriscada ou impossível.

## Como envolvo um classificador existente

```python
from jurisgrow import JurisGrowClassifier
from jurisgrow.core.features import TfidfFeaturePipeline

clf = JurisGrowClassifier.from_xgboost(
    model=modelo_existente,  # XGBClassifier já treinado
    feature_pipeline=pipeline_existente,
    class_names=["peticao_alpha", "certidao_beta", "decisao_gamma"],
)

clf.predict("documento fictício requerendo operação alfa")
# 'peticao_alpha'
```

Sem residuais, o JurisGrow é **indistinguível** do modelo base — mesmo rótulo,
mesma probabilidade. Isso é verificado por teste.

O modelo base nunca é reajustado. Não existe caminho de código que chame
`base.fit()` depois do `from_xgboost`.

### Documentos estruturados, não só texto

Se o seu classificador consome um documento estruturado através de um
`ColumnTransformer`, o JurisGrow o reaproveita como está:

```python
from jurisgrow.core.features import SklearnColumnPipeline

pipeline = SklearnColumnPipeline(
    column_transformer=transformador_ja_ajustado,
    row_builder=minha_funcao_documento_para_tabela,
)
```

## Como adiciono uma classe

```python
# 1. memória das classes antigas, a partir de dados de TREINO
clf.fit_replay(textos_de_treino, rotulos_de_treino)

# 2. a classe nova
relatorio = clf.add_class("termo_delta", texts=documentos_novos)

print(relatorio.summary())
# [ACEITO] termo_delta: F1=0.9231 (n=80) | esquecimento=+0.0111 (proxy_fpr) |
#          ativação falsa=≥0.0111 (limite inferior)
```

A operação é **transacional**. Se o treino falhar, ou se a política recusar, o
classificador continua exatamente como estava — mesmas predições, bit a bit.

## Como o esquecimento é medido

Toda `add_class()` responde duas perguntas, não uma:

```python
relatorio.new_class_f1                       # aprendemos o novo?
relatorio.forgetting                         # o antigo sofreu?
relatorio.forgetting_method                  # COMO esse número foi obtido
relatorio.false_residual_activation_rate     # quanto o residual invadiu?
relatorio.false_activation_is_lower_bound    # é estimativa ou piso?
```

### Leia o `≥` antes de confiar no número

Se o replay foi construído com os documentos em que o **modelo base** treinou —
que é o que o passo 1 acima faz, e o padrão da biblioteca — a taxa de ativação
falsa é um **limite inferior**, não uma estimativa. O base acerta quase 100%
nesses documentos, as meta-features do residual derivam dessas probabilidades,
e o residual quase não ativa falso ali. Medido em corpus com classes
sobrepostas: **0,03 relatado contra 0,44 real**.

Particionar o replay não corrige — as duas metades estão igualmente
contaminadas. Para uma medida honesta, reserve documentos das classes antigas
que o base não viu:

```python
clf.fit_replay(textos_holdout, rotulos_holdout, seen_by_base=False)
```

O `summary()` imprime `≥` sempre que o número for piso, e
`forgetting_method` diz se o esquecimento foi medido, aproximado, garantido por
construção, ou não avaliado.

A `AcceptancePolicy` recusa por padrão se o dano passar do orçamento — **F1 alto
na classe nova não compra aceitação**:

```python
from jurisgrow import AcceptancePolicy

politica = AcceptancePolicy(
    min_new_class_f1=0.85,
    max_old_false_positive_rate=0.02,  # guarda principal
    max_old_class_f1_drop=0.01,  # guarda secundária
)
relatorio = clf.add_class("termo_delta", texts=docs, policy=politica)

if not relatorio.accepted:
    for razao in relatorio.reasons:  # todas as razões, não a primeira
        print(razao)
```

A guarda principal é a **taxa de ativação falsa**, não o macro-F1. Com `n`
classes, uma única delas colapsando move o macro-F1 em `1/n` — com 31 classes
isso é 0,032, mais que o triplo de um orçamento de 0,01. A ativação falsa mede o
dano diretamente e não muda de escala com o número de classes.

## Documentos de tipo desconhecido

Um classificador *closed-set* diante de um tipo novo devolve, em silêncio, o
rótulo conhecido mais parecido — com confiança alta e acerto nulo.

```python
clf.fit_unknown_detector(proba_conhecidos, proba_novos)

p = clf.predict_detailed(documento_estranho)
print(p.label, p.reason)  # UNKNOWN low_margin
```

Os limiares são **calibrados**, nunca constantes arbitrárias. Sem calibração o
detector funciona, mas emite aviso.

## Como salvo e carrego

```python
clf.save("meu_modelo")
recarregado = JurisGrowClassifier.load("meu_modelo")
```

XGBoost em UBJ nativo, limiares e manifesto em JSON legível, replay em matriz
esparsa. O round-trip preserva rótulo **e** confiança.

O manifesto traz `contains_raw_text` explícito, para um revisor humano decidir
num relance se o artefato pode sair da instituição.

> **Segurança:** arquivos `.joblib` são desserializados com pickle e podem
> executar código arbitrário. Carregue apenas modelos de origem confiável.

## O JurisGrow envia meus dados para algum lugar?

Não. Sem telemetria, sem analytics, sem relatório remoto de erros, sem requisição
HTTP, sem chamada a LLM, sem serviço de nuvem.

Por padrão o `ReplayBuffer` guarda **vetores esparsos, não texto**, e o
`save()` **recusa** gravar texto cru sem `allow_raw_replay=True`. Predições não
retêm a entrada. O relatório de treino registra contagens e nomes de classe,
nunca conteúdo de documento.

## Tutoriais

Sete notebooks executáveis em [`notebooks/`](notebooks/) — do primeiro modelo à
adição de classes e à escolha da ordem de decisão, com dados fictícios:

```bash
pip install "jurisgrow[notebooks]"
jupyter lab notebooks/
```

## Desenvolvimento

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest                               # testes (dados sintéticos apenas)
ruff check .                         # lint
mypy jurisgrow                       # tipos
python scripts/privacy_check.py .    # varredura antes de publicar
```

A especificação completa do projeto está em
[`docs/especificacao-original.md`](docs/especificacao-original.md); o plano de
implementação por etapas, em [`docs/plano/`](docs/plano/).

## Licença

MIT.
