Skip to content

Auditoria metodologica (5 achados) + correcao DD-SIMCA + robustez de figuras/CLI - #7

Merged
ErleySC merged 38 commits into
masterfrom
historico-limpo-preview
Aug 16, 2026
Merged

Auditoria metodologica (5 achados) + correcao DD-SIMCA + robustez de figuras/CLI#7
ErleySC merged 38 commits into
masterfrom
historico-limpo-preview

Conversation

@ErleySC

@ErleySC ErleySC commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What this changes

Esta branch acumula várias sessões de trabalho científico e de robustez, culminando numa auditoria metodológica (sessão mais recente) que comparou 3 módulos do núcleo científico (chemometric_stats.py, classificadores.py, validacao_estatistica.py) equação por equação contra as publicações originais e corrigiu 5 divergências medidas empiricamente. Relatório completo e scripts reprodutíveis em docs/auditoria/AUDITORIA_METODOLOGICA_2026-08-07.md.

São várias mudanças logicamente distintas (o próprio template pede para dividir PRs assim) empilhadas na mesma branch ao longo do histórico do projeto; documentando por grupo para o revisor:

Auditoria metodológica (9 commits mais recentes, sessão de 2026-08-07):

  • A1 (crítico): teste de permutação/Wold permutava rótulos por amostra, não por grupo (mae_id) — quebrava a coerência de réplica física sob H0.
  • A2 (crítico): Selectivity Ratio usava o peso PLS w1 em vez do vetor de regressão normalizado b/‖b‖ (Rajalahti et al. 2009) — só coincidiam com 1 variável latente.
  • A3 (alto): domínio de aplicabilidade (dominio_aplicabilidade_*) usava a mesma regra retangular independente por eixo já corrigida no DD-SIMCA — unificado por reúso das funções puras extraídas para chemometric_stats.py.
  • A4 (médio, decisão do autor): OPLS-DA multiclasse construía o alvo contínuo via LDA(X,y); trocado por PLS2 multi-coluna, o caminho publicado.
  • A5 (baixo): docstring de hotelling_t2_limite corrigida (citava Fase II mas se aplicava também em contexto de Fase I); uma segunda alegação da auditoria (q_residuos_limite) foi verificada como falsa e retratada no próprio relatório.

Correção DD-SIMCA + diagnósticos complementares (P1 do CLAUDE.md):

  • DDSimca.predict() trocado de regra retangular (T²≤UCL e Q≤UCL, alpha independente por eixo) para a distância combinada publicada por Kucheryavskiy, Rodionova & Pomerantsev (2024).
  • Diagnóstico complementar via Procrustes Cross-Validation (extra opcional [robusto]).
  • Detecção robusta (mediana/MAD) de réplicas de treino atípicas — só sinaliza, nunca remove automaticamente.

Robustez de figuras e CLI (achados de auditoria anterior sobre a 1ª execução real, P10):

  • Curva DET era uma reta horizontal (np.interp com eixo decrescente sem ordenar).
  • Biplot PCA com rótulos sobrepostos e loadings redundantes.
  • np.interp sem ordenar em mais 3 sítios (dados_io, predicao, spectra_preview) — risco de predição errada em silêncio com .dx de terceiros em ordem decrescente.
  • Painel do CLI "apagava a tela" (causa real: sys.stdout trocado globalmente por contextlib.redirect_stdout, não a altura do terminal).
  • Piso de eixo log fixo escondendo a maioria dos pontos em 3 figuras diferentes.
  • Novo diagnóstico: detecção de região espectral morta/ruidosa na faixa configurada.

Why

O objetivo declarado do projeto (CLAUDE.md) é validação group-aware honesta para autenticação de óleos por FT-NIR — cada um desses achados era uma forma de o software produzir um número errado sem avisar ninguém, o pior tipo de bug em software científico. A1 em particular atingia o argumento central do projeto: o teste que produz o p-valor citável não era group-aware.

Does this change any number the software produces?

  • Yes — mudanças que alteram números:
    • A1: p-valores de permutação/Wold mudam (o nulo anterior era artificialmente estreito — falso positivo medido em 15% contra 5% nominal antes da correção).
    • A2: Selectivity Ratio muda para qualquer modelo com ≥2 variáveis latentes (a versão anterior estava congelada na resposta de 1 LV); a seleção de variáveis em selecao_variaveis.py que usa SR pode selecionar um conjunto diferente.
    • A3: decisão dentro/fora do domínio de aplicabilidade muda (rejeição medida em 11,6% contra 5% nominal antes da correção); pacote .joblib salvo por pipeline.py muda de formato (ad_t2_limite/ad_q_limitead_h0/ad_q0/ad_Nh/ad_Nq/ad_f_crit), retrocompatível para pacotes antigos.
    • A4: scores OPLS-DA multiclasse (t_pred/t_orth) mudam — alvo contínuo agora vem de PLS2 em vez de LDA.
    • DD-SIMCA (P1): aceitação/rejeição muda de regra retangular para distância combinada.
    • Todas comparadas contra a fórmula publicada ou contra uma propriedade que a define (ex.: corr(t_tp, ŷ) == 1 para SR); nenhum valor foi só "ajustado até bater".

Checklist

  • pytest -q — 672 passed, 2 skipped (663 antes desta sessão; subiu com os testes de regressão novos, nenhum caiu)
  • ruff check . — limpo (repo inteiro, incluindo docs/auditoria/)
  • Coverage não caiu (67%, reverificado)
  • Nenhum print() novo nos módulos tocados (os scripts de docs/auditoria/ usam print mas são scripts standalone de relatório, não parte do pacote)
  • Nenhum except Exception novo sem noqa: BLE001 justificado
  • Todo número escrito em docs/auditoria/AUDITORIA_METODOLOGICA_2026-08-07.md foi medido rodando os scripts, não estimado — incluindo a retratação de um achado que se provou falso na reverificação
  • Toda referência bibliográfica citada (Rajalahti 2009, Tracy-Young-Mason 1992, Winkler 2015, Trygg & Wold 2002, Kucheryavskiy 2024, Jackson & Mudholkar 1979) foi verificada por busca antes de citar

Pendente após o merge, não resolvido nesta PR (registrado no CLAUDE.md, item P11 e nota do N2): a execução real N2 precisa ser refeita — agora por 3 motivos acumulados (correção do DD-SIMCA, A1, A3) — e N3 nunca rodou com nenhuma das correções desta branch.

ErleySC and others added 30 commits August 7, 2026 10:07
…rtido

sklearn.det_curve devolve fmr DECRESCENTE; np.interp exige xp crescente e
nao ordena sozinho. A interpolacao degenerava e devolvia fnmr[-1] constante
para todo FMR>0 — toda curva DET gerada ate hoje era um artefato sem
significado, nao um resultado. Extraida interpolar_det() como funcao pura
com 3 testes de propriedade; verificado que o teste falha com o codigo
antigo. Diagonal renomeada de "Ref. diagonal" para "EER (FMR=FNMR)" —
rotulo antigo induzia a leitura errada de que a curva deveria segui-la.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dois defeitos somados deixavam o biplot ilegivel. (1) top-N por magnitude
selecionava canais VIZINHOS da mesma banda espectral (no espectro real:
5875/5883/5891/5899... = 2 bandas contadas 12x); corrigido com
selecionar_loadings_distintos() (separacao espectral minima + piso de
magnitude relativo, para nao completar a cota com ruido — o titulo passa
a mostrar "top-5" quando so' ha' 5 bandas reais em vez de forcar 12).
(2) nenhuma funcao evitava sobreposicao de rotulos; corrigido com
afastar_rotulos() (agrupa em colunas por x, empilha em y — convergencia
garantida em uma passada, com linha-guia ate a seta original). Uma
primeira versao por repulsao par-a-par iterativa oscilava e deixava 7
pares sobrepostos mesmo apos 120 iteracoes — medido, descartado.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…guras

figuras_concluidas/avisos_do_log crescem sem teto durante a execucao. Numa
corrida completa (26 figuras + varios avisos distintos) o painel passava
de 35 linhas num terminal de 24 — o Live do Rich perde o controle do
cursor quando o bloco nao cabe na janela e a tela fica preta com so' o
cursor piscando, embora o calculo continue rodando normalmente por baixo
(era exatamente o sintoma relatado como "tela preta"/"CPU baixa").

Painel agora com altura limitada: mostra os 4 avisos mais recentes + um
contador de quantos ficaram de fora, lista de figuras truncada, e
vertical_overflow="crop" no Live como rede de seguranca. Medido: pior
caso caiu de 35 para 22 linhas, dentro de um terminal padrao de 24.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nfigurada

Rodar com faixa espectral larga demais nao e' inofensivo: infla o numero
de variaveis, dilui VIP/SR, encarece a CV e da' ao modelo espaco para
ajustar ruido. diagnosticar_faixa_espectral() separa DOIS defeitos que
pedem acoes diferentes — regiao MORTA (sem sinal analitico) de RUIDOSA
(dominada por alta frequencia) — via SNR entre componente suave e residuo
por amostra, e sugere a faixa efetivamente util.

So' AVISA (no log e no resumo_modelo.txt); nunca corta sozinho, porque
mudar a faixa muda o resultado e essa decisao e' do usuario. Verificado
que nao da' falso positivo em espectro que usa a faixa inteira, e que
separa corretamente regiao morta de regiao ruidosa em cenarios sinteticos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ados_io/preview)

Auditoria pelo mesmo tipo de bug da curva DET achou 3 outros locais que
chamam np.interp sem garantir eixo crescente: dados_io.py (preenchimento
de NaN no parser JCAMP-DX), predicao.py (aplicar modelo .joblib a amostra
nova) e spectra_preview.py (previa visual). np.interp exige xp crescente
e nao ordena sozinho.

O equipamento do autor (ABB MB3600) grava numero de onda CRESCENTE, entao
isso NAO afeta nenhum resultado ja obtido com este dataset — mas um .dx
de terceiro em ordem decrescente (convencao comum em FTIR) produziria
reamostragem errada sem lancar erro. Em predicao.py isso e' o caminho
"aplicar modelo a amostra nova": e' exatamente onde uma resposta errada
sem aviso e' mais grave. Teste de regressao prova que as duas ordens de
entrada agora dao o mesmo resultado, e que o caminho antigo (sem ordenar)
de fato divergia.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…no CLAUDE.md

Atualiza CHANGELOG.md com o detalhamento das 5 correcoes desta sessao
(DET, biplot, painel, diagnostico de faixa, np.interp) e adiciona P10 ao
CLAUDE.md com a licao transversal: testes de figura verificavam so' que o
.png existia, nunca uma propriedade do conteudo — e' por isso que a DET
quebrada sobreviveu ate ser notada visualmente. Contagem de testes
atualizada (562->634).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… nao altura

A correcao anterior (limitar altura do painel) era valida mas nao atacava
a causa raiz: `console` (guaraci_theme.py) e' construido sem `file=`, entao
`Console.file` resolve `sys.stdout` DINAMICAMENTE a cada escrita. Como
`contextlib.redirect_stdout` troca `sys.stdout` GLOBALMENTE no processo
(nao por thread), enquanto `_run()` segura esse redirect em background
durante toda a execucao do pipeline, o `Live` do thread principal tambem
passava a escrever no mesmo buffer capturado — nao no terminal. Medido
isolado: 0% da saida do Live chegava ao terminal real durante o redirect,
100% ia parar no buffer de log.

Fix: capturar a referencia real de sys.stdout ANTES do redirect comecar e
fixar console._file nela (com restauracao em finally) pela duracao do
Live, imunizando o painel contra o redirect global da thread de trabalho.
2 testes de regressao provam o mecanismo antes/depois com o console real
do modulo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… pontos

Com poucas amostras puras de treino (nc=3, cenario real do dataset), o
modelo one-class fica com apenas n_comp=1 (ver _MIN_Q_RESIDUAL_DF).
Amostras de OUTRAS classes projetam quase sempre perto de zero nesse
unico eixo, que nao tem relacao com a variancia delas. Medido: 91% dos
pontos caiam abaixo do piso FIXO de 1e-2 usado no grafico, todos
empilhados na MESMA coluna de pixels — os valores reais variam de 1e-10
a 1e-2 (8 ordens de grandeza), mas o piso fixo escondia essa variacao
inteira atras de uma parede visual que parecia defeito de renderizacao.

Corrigido com _limites_log_ddsimca() (funcao pura): piso e teto do eixo
log calculados a partir dos DADOS (percentil 1/99 dos valores positivos,
com min/max de seguranca), por eixo (T2 e Q independentes — um nao
precisa esticar o outro). n_comp exposto na caixa de info do painel
(score_matrix ganhou o campo), para o padrao ficar explicado como
propriedade do modelo, nao parecer bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…a dados

Auditoria varreu as 34 funcoes fig_* do pacote por classe de defeito
(piso/limite fixo, np.interp sem ordenar, sobreposicao de rotulo, inversao
de eixo espectral, normalizacao de colormap, robustez a dado degenerado).

CORRIGIDO:
- fig_ddsimca_individuais: tinha o MESMO piso fixo de 1e-2 da versao em
  grade (é a mesma figura, salva individualmente). Passa a usar
  _limites_log_ddsimca e a expor n_comp/n_train, igual a de grade.
- fig_extra_wold: piso do eixo Y fixo em -0.5/-0.6. Bug LATENTE — Q2Y de
  rotulos permutados fica mais negativo quanto mais componentes o modelo
  usa. Medido com 13 classes: 23 LVs -> minimo -0.465 (cabe, a execucao
  atual NAO foi afetada), mas 40 LVs (o max_lvs configurado neste projeto)
  -> 80% dos pontos abaixo de -0.6, sumindo do grafico enquanto a reta de
  regressao seguia sendo calculada sobre eles. _ylim_permutacao mantem o
  piso padrao quando cabe (execucoes normais ficam identicas) e so' expande
  quando ha' ponto abaixo.

VERIFICADO E CORRETO (sem alteracao, para nao mexer no que funciona):
- fig3_outliers: clip em 1e-2/1e-12 medido em 0% dos pontos (com 23 LVs o
  T2 minimo e' 7.1) — piso nunca ativa.
- Eixo espectral invertido em TODAS as 5 figuras vs numero de onda,
  confirmado renderizando e inspecionando xlim (inclusive a propagacao via
  sharex em fig6_preprocessamento).
- Normalizacao de colormap: confusao (0..1), s-plot (-1..1) e heatmap
  (divergente ancorada no limiar) todas coerentes com a grandeza.
- Demais limites hardcoded sao de grandezas em [0,1] (acuracia, ROC,
  frequencia de selecao) — legitimos.
- Teste de estresse com dado degenerado (classe quase constante, n=6):
  nenhuma excecao e nenhum eixo silenciosamente vazio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inconsistencia apontada na auditoria anterior mas nao corrigida ainda:
esta era a unica figura DD-SIMCA com marcador de tamanho/opacidade FIXO
(s=20, alpha=0.80) em vez de parametros_scatter_adaptativos, usado por
todas as irmas (fig_sprint3_ddsimca_acceptance, fig_ddsimca_individuais).
Com 1346 amostras reais isso deixava o painel mais denso/poluido que o
resto do conjunto. Calculado uma vez fora do loop de pares (densidade
nao muda entre paineis A x B).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ha P1)

predict() aceitava um objeto se T2<=UCL(T2) E Q<=UCL(Q) independentemente
-- uma regiao retangular. O docstring da classe ja documentava essa
divergencia do metodo citado (Rodionova/Pomerantsev), mas sem a formula
exata para corrigir. Com alpha independente por eixo a rejeicao conjunta
efetiva era ~1-(1-alpha)^2~=0.0975, quase o dobro do declarado.

Pesquisa de literatura atualizada achou Kucheryavskiy, Rodionova &
Pomerantsev (2024) J. Chemometrics 38(7):e3556 -- tutorial dos proprios
autores do DD-SIMCA com a formula exata (Eq. 3-4): distancia combinada
f=(T2/h0)*Nh+(Q/q0)*Nq comparada a UM UNICO f_crit=chi2(1-alpha,Nh+Nq),
com Nh/Nq estimados dos dados por metodo dos momentos (a mesma matematica
que q_residuos_limite ja usava so' para Q, estendida a T2 tambem).

Medido em cenario sintetico controlado: regra antiga aceitava 93.85% de
uma distribuicao conhecida (deveria ~95%), regra nova aceita 96.80%;
2.95% dos pontos MUDAM de classificacao entre as duas regras. A regra
estava duplicada em 3 lugares (predict(), sensibilidade_ddsimca_logo(),
especificidade do pipeline) -- unificada: score_matrix() agora expoe
"f"/"f_crit", os 3 usos comparam contra a mesma fonte.

Figuras (fig_sprint3_ddsimca_acceptance, fig_ddsimca_individuais):
a "caixa" de duas linhas retas perpendiculares nunca foi a regiao de
aceitacao real -- agora desenham a reta diagonal unica que a distancia
combinada de fato usa (_fronteira_ddsimca()).

Golden test regravado: especificidade/n_desconhecidos do N2 sintetico
mudam na direcao esperada (ex.: Esp_A 61.9->38.1%) -- a regra antiga
super-rejeitava em geral (inclusive amostras da propria classe), inflando
especificidade como efeito colateral; a regra correta aceita mais no
total, entao a especificidade cai para um valor mais honesto.

651 testes passam (eram 644), ruff e mypy limpos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…idation

Pesquisa de literatura atualizada (small-sample robustness) achou PCV --
Kucheryavskiy/Zhilin/Rodionova/Pomerantsev (2020) Anal. Chem. 92(17):
11842-11850 e Pomerantsev/Rodionova (2021) Talanta 226:122104, mesmos
autores do DD-SIMCA, atacando "short datasets" diretamente. Integrado via
pacote opcional 'prcv' (novo extra [robusto]).

sensibilidade_ddsimca_pcv() gera um "PV-set" por reamostragem e reporta
sensibilidade sobre ele, SEMPRE ao lado do LOGO existente, nunca em vez
dele. Caveat cientifico verificado empiricamente: com todas as replicas
puras de uma classe no MESMO grupo mae_id (n_grupos=1, caso mais comum
neste dataset), o PV-set so' reproduz ruido de MEDICAO, nunca variacao
entre amostras fisicas -- PCV nao fabrica informacao que nao existe nos
dados, e o aviso reportado deixa esse limite explicito.

Testado que o split de CV agrupado por mae_id com 1 unico grupo faz
pcvpca falhar (ValueError de shape) -- fallback para leave-one-out por
amostra individual nesse caso (nao ha estrutura de grupo a proteger de
qualquer forma quando so' existe 1 grupo).

Opt-in via cfg.ddsimca_pcv (default False). Wiring completo: Config/
_CONFIG_SPEC, menu CLI (menu_modelagem) e aba do app web (modelo.py) --
os testes de alcancabilidade de campo pegaram os 2 pontos que faltavam
antes de rodar a suite completa.

657 testes passam (eram 651), ruff e mypy limpos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ipicas

Terceiro item da pesquisa de "novas tecnologias" pedida. Kucheryavskiy/
Rodionova/Pomerantsev (2024) recomendam estimadores ROBUSTOS (mediana/IQR)
para DETECTAR outliers no treino, revertendo para classicos so' depois de
removidos. Com nc=3-4 amostras puras (o regime real deste projeto),
remover uma amostra por suspeita pode derrubar o modelo inteiro abaixo do
minimo de graus de liberdade -- decisao de escopo deliberada:
_outliers_robustos_mad() (z-score modificado, Iglewicz & Hoaglin 1993) SO'
SINALIZA, nunca remove sozinho.

Verificado empiricamente no cenario real (2 replicas proximas + 1
divergente -> divergente corretamente sinalizada) e documentado
honestamente que a uniao dos 2 eixos (T2 e Q) chega a ~10% de falso
positivo mesmo em n=20 (medido: 3/30 seeds) -- T2/Q_train ja e' instavel
com so' 2 graus de liberdade residuais nesse regime, entao o aviso deve
ser lido como "vale conferir esta replica", nunca como "esta errada".

n_train do modelo continua o numero ORIGINAL de amostras sempre --
testado explicitamente que nenhuma e' removida. Exposto em
score_matrix() ("outliers_treino") e no resumo (DD-SIMCA {classe} AVISO
treino).

663 testes passam (eram 657), ruff limpo; mypy limpo nos 7 modulos puros
do gate de CI (pipeline.py tem debito de tipagem pre-existente e
identico antes/depois, confirmado via git stash -- fora do escopo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverificacao pedida ("fonte de melhorias em aberto"), nao tocada desde
07-13 apesar da propria regra do arquivo pedir reverificacao a cada
sessao. Numeros atualizados: 663 testes (634), cobertura 67% (64), 53
except amplos (51), guaraci.py 3813 linhas (3318), print() em pipeline
corrigido para 0 (tabela ainda mostrava 164, migracao ja tinha sido feita
em 07-13 mas a tabela nunca foi reverificada depois).

Achado concreto: o N2 real (PLSDA_OE_Autenticacao_..._101911, rodado
2026-08-07 10:19) precede a correcao da regra de decisao do DD-SIMCA
(commit b51f361, 13:52 do mesmo dia) em 3h30 -- os numeros de
sensibilidade/especificidade desse resumo usam a regra retangular antiga
(rejeicao efetiva ~0.0975, nao 0.05) e precisam ser reexecutados antes de
citar. N3 nunca rodou com nenhuma das correcoes desta sessao (CV,
DD-SIMCA, figuras) -- so existe uma rodada de 07-05, totalmente anterior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Compara cada metodo do nucleo (chemometric_stats, classificadores,
validacao_estatistica) contra a publicacao original e mede a divergencia
empiricamente, em vez de assumir correcao por precedente em outro
software -- mesma classe de erro do bug do DD-SIMCA corrigido em
2026-08-08.

5 achados (2 criticos, 1 alto, 2 menores), scripts reprodutiveis em
docs/auditoria/. A1 (teste de permutacao nao group-aware) e o mais grave:
falso positivo medido em 15% contra 5% nominal.
teste_permutacao/teste_wold embaralhavam rotulos por AMOSTRA, ignorando
mae_id -- apos o embaralhamento, um mesmo grupo de replica fisica ficava
com rotulos diferentes, um conjunto que nao pode existir sob H0. O nulo
resultante fica artificialmente estreito.

Medido (docs/auditoria/medir_permutacao_grupos.py, H0 verdadeiro, 12
grupos x 3 replicas, 120 repeticoes): taxa de falso positivo de 15,0%
contra 5% nominal com permutacao por amostra; 4,2% com permutacao por
grupo.

_gerar_permutacoes_rotulo() permuta a ATRIBUICAO de rotulo entre grupos
(Winkler et al. 2015, multi-level block permutation), preservando a
coerencia interna de cada mae_id. Sem `groups`, cai para permutacao por
amostra identica ao comportamento anterior (mesmo rng, mesma sequencia)
-- os testes existentes que nao passam groups continuam com resultado
numerico identico.

667 testes passam (663 + 4 novos), 2 skip -- sem regressao.
…(fecha A2)

calcular_selectivity_ratio usava o primeiro peso PLS (w1) como direcao da
projecao-alvo. Rajalahti et al. (2009, Sec. 2.2) define a projecao-alvo
sobre o VETOR DE REGRESSAO NORMALIZADO b/||b||; os dois so coincidem com
1 variavel latente.

Medido (docs/auditoria/medir_sr_ranking.py): corr(t_tp, y_hat) -- a
propriedade que define o metodo -- caia de 1.000000 exato para ~0.92 com
>=2 LVs; o SR ficava congelado na resposta de 1 LV (identico para 2, 3,
4, 6, 8 LVs); Jaccard@20 entre o ranking implementado e o de referencia
caia para 0.39 em cenario multi-interferente. Como
selecao_variaveis.py:_mask_sr_top_frac() usa o ranking de SR para
selecionar variaveis, o metodo anterior selecionava um conjunto
diferente do que a literatura selecionaria.

Y multi-coluna (one-hot, classificacao multiclasse) nao e' o caso
univariado do metodo publicado: aplica a formula exata a cada classe
(one-vs-rest) e agrega por MAXIMO, mesma logica ja usada em
teste_incerteza_martens() para agregacao multi-saida.

Testes: 2 testes de caso degenerado atualizados (mockavam x_weights_,
atributo que a funcao corrigida nao usa mais -- agora mockam coef_) e 3
novos travando a propriedade que falhava antes da correcao (corr(t_tp,
y_hat) == 1 exato p/ qualquer nº de LVs, SR bate com a formula de
referencia, agregacao multiclasse por maximo).

670 testes passam (667 + 3 novos), 2 skip -- sem regressao.
…a retangular (fecha A3)

dominio_aplicabilidade_amostras_novas decidia dentro/fora do dominio por
T2<=T2_limite E Q<=Q_limite independentemente (alpha=0.05 por eixo) -- a
MESMA regra retangular corrigida no DD-SIMCA em 2026-08-08 (fecha P1),
ainda presente aqui. Alpha conjunto efetivo inflado para ~1-(1-alpha)^2.

Medido (docs/auditoria/medir_achados.py, 40 simulacoes, amostras novas da
MESMA distribuicao do treino): rejeicao de 11.6% contra 5% nominal.
Usado em producao por predicao.py -- uma em cada nove amostras legitimas
era marcada fora do dominio.

Correcao por REUSO em vez de terceira reimplementacao: extraidas de
DDSimca para chemometric_stats.py as duas funcoes puras que definem a
distancia combinada do DD-SIMCA (Kucheryavskiy, Rodionova & Pomerantsev
2024) -- media_e_dof_momentos() e distancia_combinada() -- e
DDSimca passa a chama-las tambem (era a unica dona da logica antes).
dominio_aplicabilidade_treino/amostras_novas agora usam a mesma
distancia f<=chi2(1-alpha,Nh+Nq) em vez do teste por eixo.

Muda o pacote .joblib salvo por pipeline.py: ad_t2_limite/ad_q_limite
viram ad_h0/ad_q0/ad_Nh/ad_Nq/ad_f_crit. predicao.py atualizado
(_CHAVES_AD, colunas AD_f/AD_f_crit no lugar de AD_T2_limite/
AD_Q_limite); retrocompativel com pacotes antigos (colunas AD_* somem,
sem excecao, e' o comportamento ja existente p/ pacote incompleto).

Testes: 2 mocks que usavam DDSimca._media_e_dof/x_weights_ desnecessarios
atualizados para a funcao movida; 1 teste novo trava que a taxa de
rejeicao medida fica bem abaixo do patamar da regra retangular.

671 testes passam (670 + 1 novo), 2 skip -- sem regressao.
…5 residual

hotelling_t2_limite citava Tracy-Young-Mason (1992) afirmando validade
"for both observations within the calibration set and new observations".
TYM 1992 estabelece exatamente o oposto: Fase I (amostras do treino) usa
distribuicao Beta, Fase II (amostras novas) usa F. A funcao implementa
so' Fase II; docstring agora credita isso corretamente e documenta a
ressalva de uso em contexto de Fase I (dominio_aplicabilidade_treino,
figuras.fig3_outliers) -- impacto numerico medido como baixo para os
tamanhos de amostra deste projeto (~1.01-1.03x com n~300).

Retratacao: o relatorio original (commit 19ddbf5) tambem alegava que
q_residuos_limite atribuia sua formula a Jackson & Mudholkar (1979) por
engano. Reverificado -- a alegacao estava errada: Jackson & Mudholkar
(1979), Technometrics 21(3):341-349, e' de fato a origem da aproximacao
por casamento de momentos usada, e a citacao padrao da literatura de
PCA/quimiometria para essa formula. Nenhuma mudanca de codigo para este
item -- a atribuicao ja existente estava correta.

Sem mudanca de comportamento (docstring apenas). 671 testes passam, 2
skip.
… (fecha A4)

OPLSDAWrapper.fit() usava o 1o escore de uma LinearDiscriminantAnalysis
ajustada em (X, y_int) como alvo continuo do NIPALS PLS1, quando Y era
multiclasse (one-hot, K>1 colunas). Trygg & Wold (2002) definem OPLS
para y binario/continuo; a extensao multiclasse publicada e' OPLS/O2PLS
com Y multi-coluna via PLS2 -- um alvo derivado de X por um
classificador supervisionado (LDA usa so' a estrutura de classes em X,
ignorando a covariancia X-Y que define o eixo preditivo do OPLS) nao e'
o metodo publicado.

Decisao do autor (entre rotular como variante do Guaraci ou trocar pelo
metodo publicado): trocar por PLS2. O caminho de fallback que ja existia
(ativado so' quando a LDA falhava por matriz de dispersao singular) vira
o UNICO caminho: 1o escore Y de um PLSRegression(n_components=1)
ajustado em (X, Y) -- capta a covariancia dominante X-Y entre todas as K
classes simultaneamente. LDA e o try/except de fallback removidos.

Logica extraida para OPLSDAWrapper._alvo_continuo() (staticmethod) para
ser testavel isoladamente -- mesmo padrao usado na correcao do A2 (SR).

Testes: os 2 que exercitavam o caminho LDA (smoke test multiclasse,
teste do fallback via monkeypatch da LDA) substituidos por 3 -- smoke
test do caminho PLS2, teste de propriedade que trava _alvo_continuo
contra a formula de referencia (y_scores_ do PLS2, centrado), teste do
caso binario. MANUAL.md/VALIDATION.md atualizados (mencionavam LDA).

672 testes passam (671 + 1 liquido), 2 skip -- sem regressao. Fecha os 5
achados da auditoria metodologica de 2026-08-07 (A1-A5).
…repo inteiro)

Imports nao usados e f-strings sem placeholder nos scripts de
docs/auditoria/ -- nao pegos nos commits anteriores porque cada um so
rodou ruff check nos arquivos de src/tests tocados, nao no repo
inteiro. Autofix (ruff check --fix), sem mudanca de logica.
…everifica ESTADO ALEGADO

Tabela ESTADO ALEGADO reverificada em 2026-08-07 antes de comecar
(nenhuma divergencia na versao anterior) e depois de fechar os 5
achados A1-A5 (testes 663->672). Novo item P11 documenta os achados,
correcoes e a retratacao de uma alegacao errada no proprio relatorio de
auditoria (q_residuos_limite / Jackson & Mudholkar). Nota do N2
atualizada: A1 (permutacao) e A3 (dominio de aplicabilidade) sao mais
2 motivos, alem do DD-SIMCA ja conhecido, para reexecutar antes de
citar numeros de Classificacao.
…rada

A etapa "[6/7]" (figuras + DD-SIMCA + OPLS-DA + holdout) concentra a
maior parte do tempo real de execucao, mas so' tinha 2 marcadores de
texto OPCIONAIS ("[6b/7]"/"[6c/7]", nem sempre emitidos) entre o inicio
e o fim. Sem eles, progresso_do_log() ficava CRAVADO em 6/7=0,857
durante toda essa fase -- o painel do CLI (e a barra do app web, mesma
funcao compartilhada) parecia travado, mesmo com figuras sendo salvas
visivelmente no log.

Medido reproduzindo o mecanismo exato do painel (thread em background +
contextlib.redirect_stdout, docs/auditoria/medir_bug_progresso_cli.py):
96,1% das amostras de progresso presas nesse numero antes da correcao,
31,1% depois (o que resta e' o platô legitimo perto do fim da etapa).

Correcao: progresso_do_log() ganha um parametro opcional
`total_figuras_planejadas` (retrocompativel -- None preserva o
comportamento antigo exato). Quando fornecido e a etapa atual e' a 6,
soma um bonus fracionario proporcional a
len(figuras_concluidas(txt))/total_figuras_planejadas, avancando o
progresso a cada figura salva em vez de so' nos 2 marcadores esparsos.
Nunca regride, nunca ultrapassa o teto global 0.99. CLI (guaraci.py) e
app web (app_tabs/modelo.py) atualizados para passar a contagem de
figuras planejadas, ja calculada em ambos antes da execucao comecar.

Bonus: "[6b/7]"/"[6c/7]" (ja existiam no log do pipeline mas nao eram
reconhecidos -- so' a etapa 7 tinha rotulo especifico para sub-passos)
agora mostram rotulo especifico em vez do generico da etapa.

677 testes passam (672 + 5 novos), 2 skip -- sem regressao.
Achado durante checkup geral de interface (pedido explicito, 2026-08-07):
_input() engole EOFError/KeyboardInterrupt internamente e devolve "" --
no loop principal de main(), "" nao bate com NENHUMA opcao de menu, cai
no ramo "invalida" + _pause() (tambem EOF-safe) e o loop volta a
cls()+ler de novo, sempre "" de novo em EOF permanente. O
try/except (EOFError, KeyboardInterrupt) que ja existia ao redor da
leitura nunca disparava, porque a excecao ja tinha sido engolida por
_input() antes de chegar la.

Reproduzido com stdin vazio: >350 redesenhos de tela em 8s sem
terminar, cada um chamando os.system("cls") (spawna subprocesso). Afeta
qualquer invocacao nao-interativa (stdin redirecionado de arquivo
vazio/pipe fechado, sessao SSH caindo, automacao/CI alimentando uma
sequencia fixa de comandos que termina antes do 'Q') -- trava
consumindo CPU sem jeito de sair exceto matar o processo de fora.

Corrigido trocando a chamada por input() direto nesse UNICO ponto
(main(), a leitura do menu principal) -- deixa o EOFError propagar ate
o handler que ja existia, em vez de _input() engoli-lo primeiro. Os
demais ~15 pontos que chamam _input()/_ask()/_pause() continuam
corretos como estao: sao menus que ja tratam "" (o default em EOF) como
"voltar", entao o engolimento de excecao la e' intencional.

Teste de regressao (in-process, builtins.input mockado para sempre
levantar EOFError, cls() mockado p/ nao spawnar subprocesso durante o
teste): trava a propriedade que falhava antes -- main() tem que
RETORNAR em bounded time, nao girar para sempre.

678 testes passam (677 + 1 novo), 2 skip -- sem regressao.
Multiplicador de minutos do GitHub Actions: 1x Linux / 2x Windows / 10x
macOS. A matriz cheia (10 combinacoes -- Ubuntu x4 versoes, Windows x4,
macOS x2) rodava por INTEIRO a cada push de PR, incluindo os 2 jobs
macOS (10x cada) -- caro demais para iterar numa conta privada com
cota limitada, que se esgotou.

Em `pull_request`: 3 combinacoes (Ubuntu 3.10/3.13 -- pontas da faixa
suportada -- + 1 Windows 3.11, sem macOS). Em `push` p/ master/main
(uma vez por merge, nao uma vez por commit de PR): matriz cheia
mantida, macOS incluso -- e' o gate real antes de entrar na branch
principal.

Selecao via `github.event_name == 'pull_request' && fromJSON(...) ||
fromJSON(...)` no `matrix.include` (padrao documentado do GHA p/
matriz condicional por evento). Validado localmente: YAML parseia
(PyYAML) e os 2 blobs JSON embutidos sao validos e batem com as
combinacoes esperadas (3 na PR, 10 no push) -- nao foi possivel
disparar um run real p/ verificar em execucao (cota zerada); um erro
de sintaxe apareceria como anotacao de "workflow invalido" no GitHub
sem gastar minutos, entao o pior caso de uma falha aqui e' barato de
detectar.

lint/typecheck (ubuntu, 1 versao cada, ja baratos) nao mudaram.
…ifico

P6 (CLAUDE.md) migrou pipeline.py para logging em 2026-07-13, mas a
tabela ESTADO ALEGADO afirmava (incorretamente, nunca reverificado)
que "os demais modulos ja usavam logging desde antes". Reverificando
com um grep correto (excluindo falsos positivos de console.print(), que
o grep original nao filtrava): chemometric_stats.py e
validacao_estatistica.py -- os 2 dos "4 modulos do nucleo" (P4) que
ainda tinham print() -- tinham 10 chamadas ao todo (classificadores.py
e preprocessamento.py ja estavam limpos).

Print() num modulo de calculo puro nao pode ser silenciado, redirecionado
ou associado a um logger/nivel configuravel -- o mesmo problema que a
migracao de pipeline.py resolveu la, so' que aqui. Como
teste_permutacao/teste_wold (os 8 casos em validacao_estatistica.py) so
sao chamados de dentro de executar() (que ja chama log.py:configurar()
antes de qualquer coisa), a saida continua identica em producao -- so
passa a ser roteavel/silenciavel.

log.warning()/log.error() usados para os 2 avisos de taxa de falha (>30%
ou 0 iteracoes validas), log.info() para o resto (progresso rotineiro).
Mensagens convertidas para %-lazy formatting (padrao logging), texto
inalterado.

678 testes passam, 2 skip -- sem regressao (nenhum teste dependia do
texto impresso via capsys).
…stsq)

MSC.transform() resolvia, POR AMOSTRA, uma regressao de 2 parametros
(a, b tal que X_i ~ a + b*ref) via np.linalg.lstsq num loop Python --
funciona, mas e' regressao linear simples (1 preditor + intercepto),
que tem forma fechada:
    b = Cov(ref, X_i) / Var(ref)
    a = mean(X_i) - b * mean(ref)
Resolvida para TODAS as amostras de uma vez via operacoes matriciais.

Verificado numericamente equivalente ao lstsq por amostra (oraculo
independente mantido nos testes): 20 casos aleatorios + casos
estruturados, diff < 1e-8. Medido em escala real do projeto (934
amostras x 8192 pontos, mesma ordem de grandeza do dataset FT-NIR):
1.5x mais rapido (0.112s -> 0.076s) -- chamado por fold/permutacao
durante CV, entao o ganho composto ao longo de uma corrida completa e'
maior que o numero isolado sugere, mas nao medi essa composicao,
entao nao reporto um numero para ela.

Unica mudanca de comportamento, documentada e testada: referencia de
treino com variancia ~0 (espectro medio CONSTANTE -- nao ocorre com
dado real). Regressao mal-posta; lstsq antigo dava a solucao de norma
minima via SVD (artefato numerico sem significado cientifico definido);
versao nova cai no MESMO fallback ja usado por amostra quando b~=0
(so' subtrai a media), mais previsivel.

3 testes novos: equivalencia com lstsq por amostra, recuperacao de
coeficientes a/b conhecidos por construcao, caso degenerado sem
NaN/Inf. 681 testes passam (678 + 3 novos), 2 skip -- sem regressao.
…08-07)

spectra_preview.py (previa de espectros nas abas Data/Preprocessing do
app web) nunca teve teste -- achado registrado na auditoria
metodologica como divida de engenharia observada, nao corrigido
naquela rodada.

12 testes cobrindo os 3 casos de uso: preview_espectros_dx (estrutura
multi-pasta, max_por_classe, pasta vazia -> None, pasta plana usa o
proprio nome como rotulo, arquivo .dx corrompido excluido sem derrubar
os demais -- mesmo espirito do achado P10, reamostragem para grade de
referencia diferente), preview_espectros_csv (leitura numerica + coluna
de classe, filtro por faixa de wn, colunas nao-numericas -> None,
coluna de classe ausente -> rotulo curinga "?"), plot_espectros_media
(1 linha por classe, inversao de eixo com wavenumber decrescente).

Os 2 `except Exception` externos (fallback para erro de I/O
verdadeiramente inesperado) ficam sem cobertura de proposito -- forcar
isso exigiria mockar Path.iterdir()/pd.read_csv so' para acionar uma
linha, o tipo de cobertura que P4 (CLAUDE.md) explicitamente desaconselha.

693 testes passam (681 + 12 novos), 2 skip -- sem regressao.
…orio de instalacao

config.yaml, perfis/, .cli_wizard_done (idioma), .cli_modo_usuario e
codigos_usuario.json eram gravados DENTRO de _BASE_DIR -- o diretorio
de INSTALACAO do pacote (onde guaraci.py em si vive). Quebra em
qualquer instalacao read-only (pip de sistema, imagem Docker, alguns
`pip install --user`): salvar_config(cfg, str(_CFG_PATH)) logo antes de
rodar o pipeline nao tinha NENHUMA guarda contra isso, e derrubava o
CLI com um PermissionError bem na hora de rodar a analise, no meio de
uma sessao interativa.

Movido para _USER_DIR = Path.home() / ".guaraci" -- gravavel em
praticamente qualquer instalacao. _BASE_DIR continua existindo, agora
so' para o recurso realmente somente-leitura que o pacote traz consigo
(CITATION.cff).

Migracao: _migrar_estado_legado(), chamada uma vez no inicio de main()
(nao na importacao do modulo -- import nao deve escrever no HOME de
quem so' esta importando, ex.: testes), copia o que existir no local
antigo e ainda nao existir no novo. NUNCA sobrescreve, NUNCA apaga a
origem. Verificado com o ambiente real: config.yaml/.cli_modo_usuario/
perfis/ migrados com conteudo identico, arquivos antigos intactos.

Defesa em profundidade: os 4 pontos de escrita que ja tinham guarda
(_set_lang, _set_modo_usuario, _salvar_cod, _exportar_csv) ganharam
_USER_DIR.mkdir() antes do write; o unico ponto SEM guarda alguma
(salvar_config antes de _rodar_pipeline) ganhou try/except + aviso
visivel em vez de deixar o traceback subir.

Testes: EOF (achado anterior) atualizado para isolar os 6 caminhos
novos, nao so' os 2 de antes. Fixture _modo_iniciante_limpo (gap
pre-existente, achado ao mexer nesta area) agora isola _MODO_FLAG --
antes escrevia de verdade dentro do checkout do pacote a cada rodada
de teste. +4 testes novos para _migrar_estado_legado (copia o que
falta, nao sobrescreve o que ja existe, nao lanca sem nada a migrar).

697 testes passam (693 + 4 novos), 2 skip -- sem regressao.
…TICO)

GUARACI_DISABLE_MODEL_UPLOAD=1 (mitigacao documentada em SECURITY.md
para deploy publico) desabilita o uploader de .joblib, mas o campo de
texto livre "Or local path to model" continuava visivel -- e um
visitante remoto podia (1) subir um pickle disfarcado de "modelo.csv"
pelo uploader de CSV da aba Dados, NAO coberto pela mesma flag,
para um caminho previsivel (pasta temp compartilhada, nome fixo), e
(2) colar esse MESMO caminho no campo de caminho local da aba Predicao
(joblib.load nao liga p/ extensao, so' bytes) -- RCE remota sem
autenticacao, apesar da mitigacao documentada estar ativa.

Causa raiz de design: um campo de texto num app web publico nunca e'
"so' o operador digita" -- qualquer visitante alcanca. A suposicao
original (comentario em app_quimiometria.py) nunca foi verdadeira para
uma aplicacao multi-usuario.

Correcao 1 (fecha o bypass direto): quando upload_bloqueado=True, o
campo de caminho local tambem fica oculto, nao so' o uploader --
nesse modo a aba Predicao nao carrega nenhum modelo pela UI web.

Correcao 2 (segunda camada de defesa, independente): novo
app_logic.caminho_upload_temp() -- funcao pura testada isoladamente --
isola uploads por sessao (uuid aleatorio via st.session_state, nunca
exposto ao cliente) em vez de um caminho fixo/previsivel compartilhado
entre TODOS os visitantes. Fecha o "caminho previsivel" que o bypass
explorava mesmo se alguem reintroduzir o campo de caminho local no
futuro, e corrige de brinde uma condicao de corrida real (2 sessoes
concorrentes se pisando no mesmo arquivo temp) que existia independente
do bypass de RCE. Basename-only (bloqueia path traversal) preservado.
Usada em dados.py (upload de CSV) e predicao.py (upload de .joblib).

Relatorio completo em docs/auditoria/AUDITORIA_SEGURANCA_2026-08-07.md
(achados S1-S3; S3 e' um commit separado).

701 testes passam (697 + 4 novos: bloqueio de path traversal, isolamento
por sessao, mesma sessao/mesmo nome reusa caminho, default sem `base`),
2 skip -- sem regressao.
ErleySC added 8 commits August 7, 2026 18:44
…AIXA)

guaraci demo, ao abrir a pasta de resultados: os.system(f'open
"{pasta_run}"') / xdg-open equivalente interpolava o caminho direto
numa string de shell. pasta_run e' sempre gerado internamente neste
caminho de codigo (nome de pasta do demo, nunca influenciado por input
externo) -- nao explora'vel hoje, mas e' exatamente o padrao que vira
injecao de comando real se um caminho influenciado por usuario
(ex.: um "tag" digitavel livremente) algum dia alimentar esta mesma
linha; um diretorio com aspas no nome ja quebraria a citacao.

subprocess.run(["open", str(pasta_run)]) -- lista de argumentos nunca
passa por um shell, elimina a classe de vulnerabilidade por completo,
nao so' o caso de uso atual. Ver
docs/auditoria/AUDITORIA_SEGURANCA_2026-08-07.md (achado S3).
…recao S1/S2

CHANGELOG.md e' o historico de versoes ativamente mantido do projeto
(uma entrada por mudanca logica, convencao ja estabelecida) -- os 15
commits desta sessao (auditoria metodologica A1-A5, 2 bugs de CLI,
matriz de CI, print->log, MSC vetorizado, cobertura de spectra_preview,
migracao de _CFG_PATH, auditoria de seguranca S1-S3) nao estavam
refletidos nele. 7 entradas novas, mesmo formato/nivel de detalhe das
entradas existentes.

SECURITY.md descrevia o comportamento ANTERIOR a' correcao do achado
S1 (dizia que GUARACI_DISABLE_MODEL_UPLOAD=1 "aceita caminhos locais
controlados pelo operador" -- exatamente a suposicao que o achado S1
mostrou ser falsa para um app web publico). Atualizado para descrever
o comportamento atual (campo de caminho local tambem desligado; uploads
isolados por sessao) e linkar o relatorio da auditoria.
… testes)

Fim de sessao: auditoria metodologica (P11) + checkup de interface (2
bugs de CLI) + auditoria de seguranca (S1-S3) + itens de debito tecnico
(print->log, MSC vetorizado, cobertura spectra_preview, migracao
_CFG_PATH). 17 commits ao todo. Numeros reverificados rodando os
comandos de verdade, nao copiados de memoria: testes 701 (era 672 na
ultima atualizacao), cobertura src/guaraci 70% (era 67%), mypy limpo
nos 7 modulos puros (nao verificado antes nesta sessao), pip-audit sem
CVE conhecida via OSV (API do PyPI instavel na rede local), gate de
cobertura do nucleo cientifico (P4) reverificado separadamente: 96%
agregado, validacao_estatistica.py exatamente no piso de 95%.
…S4, CI local

REVISAO PRE-RELEASE. Estado verificado rodando os comandos, nao de
memoria: 701 testes passam (2 skip), ruff limpo no repo inteiro, mypy
limpo nos 7 modulos puros, gate do nucleo cientifico (P4) em 96%,
pip-audit sem CVE via OSV, versao consistente em pyproject/config.py/
CITATION.cff (31.9.0, sem version drift).

[P7 DESATUALIZADO] O checklist afirmava que requirements-lock.txt "nao
existe ainda". FALSO -- existe, tem 117 linhas de pins exatos gerados de
um venv real e testado, inclui prcv==1.2.1 do extra [robusto], e os pins
batem exatamente com o ambiente instalado. Criado em f0387e7, regenerado
em 20e846f e 3784c45. Tambem faltava [robusto] na lista de extras.
Corrigido: o unico item realmente em aberto do P7 e' a publicacao no
PyPI em si (depende da conta do autor).

[S4 -- NOVO ACHADO, requer acao do autor] Levantado ao avaliar se o repo
pode ser tornado publico (via mais direta p/ resolver a cota do Actions,
que e' gratuito e ilimitado em repo publico). TUDO que esta no GitHub
hoje esta limpo: origin/master, origin/historico-limpo-preview e as 11
tags remotas tem 0 arquivos .dx. Mas a branch `master` LOCAL (26a8f5b,
nunca realinhada apos a reescrita de historico -- o remoto esta em
88caa27) ainda carrega os 48 espectros reais do TCC. Enquanto esse ref
existir, um `git push origin master` / `git push --all` publica o
dataset. Documentado com a acao recomendada e a ressalva de que objetos
de historico reescrito podem persistir no servidor ate' GC -- por isso a
alternativa mais segura p/ publicar e' um repo novo, nao tornar publico
o que ja existiu privado com o dado dentro. NAO e' corrigivel por
commit: e' estado de ref local + decisao do autor/orientador sobre o
dataset.

[CI LOCAL] scripts/ci_local.sh roda os 4 gates do workflow na mesma
ordem (ruff -> mypy -> pytest+cobertura>=60% -> nucleo>=95%). Enquanto a
cota do Actions estiver esgotada, e' a unica verificacao automatizada
disponivel. Documenta explicitamente que NAO substitui a matriz de
SO/versao do CI -- bug especifico de plataforma continua so' aparecendo
la'.
Decisao explicita do autor apos tornar o repositorio publico: nada que
vincule o projeto a instituicao ou grupo de pesquisa, sem excecao --
incluindo os metadados academicos.

24 referencias em 12 arquivos:
- CITATION.cff: remove `affiliation:` dos 2 blocos de autor e a mencao
  no abstract.
- paper/paper.md (submissao JOSS): afiliacao vira "Independent
  Researcher, Brazil"; agradecimento passa a creditar "the research
  group" sem nomea-lo.
- README.md / README.pt-br.md: tira a instituicao da linha de autoria e
  da descricao de origem do projeto.
- UI/relatorios: os defaults "GEAAp / UFPA" de campo Institution viram
  string vazia (o usuario preenche o proprio); placeholder generico;
  textos da aba Sobre (web e CLI) sem a mencao ao programa de pesquisa.
- reports.py: template LaTeX de agradecimento generalizado.
- dados_io.py: comentarios que descreviam o dataset de origem.
- resultados_io.py, tests/test_reports.py: fixtures/textos.
- CLAUDE.md, docs/CHANGELOG.md: o repo agora e' publico, entao esses
  arquivos internos tambem sao visiveis. CLAUDE.md ganha diretriz
  explicita para nao reintroduzir a mencao no futuro.

Verificado: varredura por geaap|ufpa|pibic|"universidade federal" em
todos os arquivos versionados nao retorna nada. CITATION.cff continua
YAML valido. 701 testes passam, 2 skip, ruff limpo.

NAO altera a versao (segue 31.9.0) -- v1.0.0 fica para quando as
pendencias estiverem fechadas, conforme decisao do autor.
…ar implantada

O repositorio passou a ser PUBLICO com a correcao do achado S1 (bypass
de RCE via pickle) commitada apenas na branch da PR #7 -- a branch
`master`, que e' a default do repo e de onde o deploy publico e'
servido, AINDA tem o codigo vulneravel (verificado: o campo "local
path" continua fora da guarda `if upload_bloqueado` em
origin/master:src/guaraci/app_tabs/predicao.py).

Ou seja: o relatorio de auditoria, agora publico, descrevia o roteiro
completo de exploracao contra um alvo que ainda nao foi corrigido em
producao. Isso inverte o proposito do documento -- de registro de
auditoria para manual de ataque.

Passo a passo removido de docs/auditoria/AUDITORIA_SEGURANCA_2026-08-07
.md (substituido por um resumo do IMPACTO, sem a receita), e das copias
que o repetiam: docs/CHANGELOG.md, docstring de
app_logic.caminho_upload_temp, comentarios em app_tabs/predicao.py e
app_tabs/dados.py, cabecalho de teste em tests/test_app_logic.py. A
correcao em si, os testes e a descricao do impacto continuam intactos --
so' a receita saiu.

O detalhe permanece no historico do Git (commit fbab311 e sua mensagem)
para quem precisar auditar a correcao, e a nota no relatorio instrui a
reintroduzi-lo depois que master + deploy estiverem corrigidos.

701 testes passam, ruff limpo.
…cleo caia p/ 94%

Primeira execucao REAL do CI apos o repositorio virar publico (Actions
e' gratuito em repo publico) expos uma falha que estava LATENTE:
lint/typecheck/paper passam, mas os 3 jobs de teste falhavam no "Gate de
cobertura do nucleo cientifico (>=95%)" com 94%.

Causa raiz: classificadores.py dava 88% no CI contra 96% local. A
diferenca e' `sensibilidade_ddsimca_pcv()` (~37 statements), que so'
executa com o pacote opcional `prcv` instalado. O CI instala a partir de
requirements.txt, e `prcv` nunca foi adicionado ali quando o PCV foi
implementado (commit 3784c45) -- so' entrou no extra [robusto] do
pyproject.toml. Sem o pacote, os testes de PCV caem no
`pytest.importorskip("prcv")` e pulam, deixando a funcao inteira
descoberta.

Ficou invisivel porque o CI esteve bloqueado por cota de minutos entre
a adicao do PCV e 2026-08-16 -- o ultimo CI verde (88caa27, master) e'
anterior ao PCV existir. Exatamente o tipo de regressao que o gate do
P4 existe para pegar, e que so' apareceu quando o CI voltou a rodar.

requirements.txt e' o manifesto de deploy e declara ser o superconjunto
de todos os extras (nucleo + web + relatorios + benchmark); `robusto`
simplesmente foi esquecido. prcv exige apenas Python >=3.7 e nao tem
dependencias declaradas -- seguro em toda a matriz 3.10-3.13.
Executado antes de qualquer push acidental poder expor o dataset agora
que o repositorio e' publico:

1. Confirmado que o dataset real vive FORA do repo (1741 .dx em
   'dados oleos/Por oleos'); a pasta dados/ do repo esta vazia e
   gitignored. Os 48 arquivos no historico eram copias antigas --
   remove-los nao perde dado de pesquisa.
2. Backup ANTES de destruir: git bundle dos 185 commits exclusivos do
   master local, gravado fora do repositorio (12 MB) e verificado com
   `git bundle verify` -- "records a complete history".
3. git branch -f master origin/master (realinha ao remoto limpo).
4. reflog expire + gc --prune=now (remove objetos orfaos).

Verificado depois: 0 arquivos .dx/.jdx alcancaveis de qualquer ref
local, 0 objetos desse tipo no banco de objetos, .git reduzido a 5 MB.
O remoto ja estava limpo (todas as branches + 11 tags, verificado ref a
ref antes de comecar).
@ErleySC
ErleySC merged commit a408d44 into master Aug 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant