Auditoria metodologica (5 achados) + correcao DD-SIMCA + robustez de figuras/CLI - #7
Merged
Conversation
…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.
…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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 emdocs/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):
mae_id) — quebrava a coerência de réplica física sob H0.w1em vez do vetor de regressão normalizadob/‖b‖(Rajalahti et al. 2009) — só coincidiam com 1 variável latente.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 parachemometric_stats.py.hotelling_t2_limitecorrigida (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).[robusto]).Robustez de figuras e CLI (achados de auditoria anterior sobre a 1ª execução real, P10):
np.interpcom eixo decrescente sem ordenar).np.interpsem ordenar em mais 3 sítios (dados_io,predicao,spectra_preview) — risco de predição errada em silêncio com.dxde terceiros em ordem decrescente.sys.stdouttrocado globalmente porcontextlib.redirect_stdout, não a altura do terminal).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?
selecao_variaveis.pyque usa SR pode selecionar um conjunto diferente..joblibsalvo porpipeline.pymuda de formato (ad_t2_limite/ad_q_limite→ad_h0/ad_q0/ad_Nh/ad_Nq/ad_f_crit), retrocompatível para pacotes antigos.t_pred/t_orth) mudam — alvo contínuo agora vem de PLS2 em vez de LDA.corr(t_tp, ŷ) == 1para 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, incluindodocs/auditoria/)print()novo nos módulos tocados (os scripts dedocs/auditoria/usamprintmas são scripts standalone de relatório, não parte do pacote)except Exceptionnovo semnoqa: BLE001justificadodocs/auditoria/AUDITORIA_METODOLOGICA_2026-08-07.mdfoi medido rodando os scripts, não estimado — incluindo a retratação de um achado que se provou falso na reverificaçãoPendente 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.