11#!/usr/bin/env python3
22# -*- coding: utf-8 -*-
33"""
4- NoologicalScanner v2.0 — Scanner Epistemologico Amplificado
5- =============================================================
4+ NoologicalScanner v3.0 — Scanner Epistemologico com Negacao + Word-Boundary
5+ ===========================================================================
6+ v3.0 — 2026-06-08 — Refinado com correcoes de precisao (SPEC-028)
67v2.0 — 2026-06-07 — Refinado e Amplificado
8+ v1.0 — 2026-06-06 — Original
79
8- Melhorias sobre v1.0:
10+ Melhorias v3.0 sobre v2.0:
11+ 1. _negation_filter() — remove sentencas negadas antes do keyword matching
12+ 2. _word_boundary_match() — evita falsos positivos por substring
13+ (ex: "control" nao casa mais com "controle")
14+ 3. keyword_map expandido: 10 dimensoes (antes 4) com keywords especificas
15+ 4. Pipeline documentado: negacao → ENRICHED_KW → TextAnalyzer → keyword_map → fallback
16+ 5. Metodos v1.0 marcados como @deprecated
17+
18+ Melhorias v2.0 sobre v1.0:
919 1. Pesos adaptativos por dominio (psicologia, economia, computacao, saude, educacao)
1020 2. Integracao com TextAnalyzer (frequencia de palavras -> validacao)
1121 3. Correlacao cruzada entre dimensoes (heatmap data)
1626 8. Analise de tendencia (comparacao multi-scan)
1727
1828Conceito original: interlocutor anonimo (2026)
19- v1.0: Marcelo Claro Laranjeira
20- v2 .0: Marcelo Claro Laranjeira — refinado e amplificado
29+ v1.0-v2.0 : Marcelo Claro Laranjeira
30+ v3 .0: Marcelo Claro Laranjeira — refinado com SPEC-028 (SDD+TDD, 14 CTs)
2131"""
2232
2333from __future__ import annotations
@@ -151,8 +161,54 @@ class NoologicalScanner:
151161
152162 Complementa o AcademicAuditTrail (que identifica ERROS)
153163 com uma camada que identifica INCOMPLETUDES.
164+
165+ Pipeline de detecção (v3.0):
166+ 1. _negation_filter() — remove sentenças negadas ("sem X", "ausência de X")
167+ 2. _category_present_v2() — ENRICHED_KW (keywords + sinônimos + n-gramas)
168+ 3. _category_present() — keyword_map específico por dimensão
169+ 4. Fallback genérico — word matching com word-boundary (\b )
154170 """
155171
172+ # ─── Negation patterns (v3.0) ────────────────────────────────────────
173+ NEGATION_PATTERNS : list [str ] = [
174+ r'\bsem\s+\w+(?:\s+(?!sem\b|aus[eê]ncia\b|n[aã]o\b)\w+){0,3}\b' , # "sem X" — non-greedy, evita capturar proximo "sem"
175+ r'\baus[eê]ncia\s+de\s+\w+(?:\s+\w+){0,3}\b' , # "ausência de X"
176+ r'\bn[aã]o\s+\w+(?:\s+\w+){0,3}\b' , # "não randomizado"
177+ r'\binexist[eê]ncia\s+de\s+\w+(?:\s+\w+){0,3}\b' ,
178+ r'\bdesprovido\s+de\s+\w+(?:\s+\w+){0,3}\b' ,
179+ r'\bcarente\s+de\s+\w+(?:\s+\w+){0,3}\b' ,
180+ ]
181+
182+ @staticmethod
183+ def _negation_filter (corpus : str ) -> str :
184+ """Remove do corpus sentencas com padrões de negacao (v3.0).
185+
186+ Evita falsos positivos como:
187+ "sem grupo controle" -> "controle" detectado
188+ "ausencia de randomizacao" -> "randomiz" detectado
189+ """
190+ import re
191+ filtered = corpus
192+ for pattern in NoologicalScanner .NEGATION_PATTERNS :
193+ filtered = re .sub (pattern , ' ' , filtered , flags = re .IGNORECASE )
194+ # Remove extra whitespace
195+ return re .sub (r'\s+' , ' ' , filtered ).strip ()
196+
197+ @staticmethod
198+ def _word_boundary_match (keyword : str , corpus : str ) -> bool :
199+ """Keyword matching com word-boundary (\b ) — v3.0.
200+
201+ Evita falsos positivos como:
202+ "control" ⊂ "controle" (substring)
203+ "randomiz" ⊂ "randomizacao" (substring)
204+ """
205+ import re
206+ # Para keywords multi-palavra, usa match literal
207+ if ' ' in keyword :
208+ return keyword in corpus
209+ # Para keywords de raiz (ex: "control", "randomiz"), usa \b
210+ return bool (re .search (r'\b' + re .escape (keyword ) + r'\w*' , corpus , re .IGNORECASE ))
211+
156212 def __init__ (self , dimensions : dict [str , KnowledgeDimension ] | None = None ):
157213 self .dimensions = dimensions or EPISTEMOLOGICAL_DIMENSIONS
158214 self .scan_results : dict [str , Any ] = {}
@@ -250,30 +306,43 @@ def _extract_corpus(self, audit_trail: Any) -> str:
250306
251307 def _category_present_v2 (self , category : str , corpus_lower : str ,
252308 dim_key : str , text_analyzer : Any = None ) -> bool :
253- """Detecção enriquecida v2: keywords + sinonimos + frequencia (TextAnalyzer)."""
309+ """Detecção enriquecida v3.0: negação → ENRICHED_KW → TextAnalyzer → keyword_map → fallback.
310+
311+ Pipeline de precedência:
312+ 1. _negation_filter() — remove sentenças negadas
313+ 2. ENRICHED_KW — keywords enriquecidas com sinonimos e n-gramas
314+ 3. TextAnalyzer — validação por frequência de palavras
315+ 4. _category_present() — keyword_map específico por dimensão
316+ 5. Fallback genérico — word matching com \b boundary
317+ """
254318 cat_lower = category .lower ()
255- # Enriched keyword map
319+ # v3.0: Remove sentencas negadas antes do matching
320+ clean_corpus = self ._negation_filter (corpus_lower )
321+ # Enriched keyword map (camada 1)
256322 if dim_key in ENRICHED_KW :
257323 for kw_cat , keywords in ENRICHED_KW [dim_key ].items ():
258324 if kw_cat in cat_lower :
259- return any (kw in corpus_lower for kw in keywords )
260- # TextAnalyzer frequency validation
325+ # v3.0: word-boundary matching
326+ return any (self ._word_boundary_match (kw , clean_corpus ) for kw in keywords )
327+ # TextAnalyzer frequency validation (camada 2)
261328 if text_analyzer and hasattr (text_analyzer , "word_counts" ):
262329 words = cat_lower .split ()
263330 found = sum (1 for w in words if len (w ) > 3 and w in text_analyzer .word_counts )
264331 return found >= len (words ) * 0.4
265- # Fallback: original keyword matching
266- return self ._category_present (category , corpus_lower , dim_key )
332+ # Fallback: original keyword matching (camada 3)
333+ return self ._category_present (category , clean_corpus , dim_key )
267334
268335 def _category_present (self , category : str , corpus_lower : str , dim_key : str ) -> bool :
269- """Verifica se uma categoria está presente no corpus .
336+ """v3.0: Keyword matching com word-boundary ( \\ b) + 5 novas dimensoes .
270337
271- Usa casamento semântico por palavras-chave específicas de cada dimensão.
338+ Usa casamento semantico por palavras-chave especificas de cada dimensao.
339+ v3.0: Adicionadas keywords para niveis_analise, temporalidade, populacao,
340+ dados, dominios (antes caiam no fallback generico).
272341 """
273342 cat_lower = category .lower ()
274343
275- # Palavras-chave por dimensão
276- keyword_map = {
344+ # Palavras-chave por dimensão (v3.0: expandido para 10 dimensoes)
345+ keyword_map : dict [ str , dict [ str , list [ str ]]] = {
277346 "paradigmas" : {
278347 "positivista" : ["positiv" , "quantitativ" , "experimental" , "hipotese" , "mensura" ],
279348 "interpretativista" : ["interpretativ" , "qualitativ" , "fenomenolog" , "compreens" ],
@@ -318,20 +387,77 @@ def _category_present(self, category: str, corpus_lower: str, dim_key: str) -> b
318387 "teleológico" : ["teleolog" , "proposit" , "finalidad" , "objetivo" ],
319388 "pragmático" : ["pragmat" , "aplic" , "util" , "pratico" , "funcional" ],
320389 },
390+ # ─── v3.0: novas dimensões com keywords específicas ───────
391+ "niveis_analise" : {
392+ "individual" : ["individu" , "intrapsiquic" , "sujeito" , "self" , "autoconsci" ],
393+ "interpessoal" : ["interpessoal" , "relacional" , "vincul" , "terapeut" ],
394+ "grupal" : ["grupal" , "organizacional" , "equipe" , "grupo" , "coletiv" ],
395+ "comunitário" : ["comunitari" , "comunidade" , "territor" , "local" ],
396+ "sistêmico" : ["politic" , "governanc" , "politica publica" , "legislac" ],
397+ "neurobiológico" : ["neurobiolog" , "neurocien" , "amigdala" , "cortex" ],
398+ "cultural" : ["cultur" , "antropolog" , "etnograf" , "intercultur" ],
399+ },
400+ "temporalidade" : {
401+ "transversal" : ["transversal" , "cross-sectional" , "amostra unica" ],
402+ "longitudinal curto" : ["follow-up" , "pre-post" , "pre post" , "seguimento" ],
403+ "longitudinal longo" : ["longitudinal" , "coorte" , "prospectiv" , "acompanhamento" ],
404+ "histórico" : ["retrospectiv" , "histor" , "arquiv" , "documental" , "passado" ],
405+ "prospectivo" : ["preditiv" , "prognost" , "futuro" , "previs" ],
406+ "desenvolvimental" : ["desenvolviment" , "ciclo de vida" , "life span" , "life-span" ],
407+ },
408+ "populacao" : {
409+ "adultos" : ["adult" , "meia-idade" , "meia idade" ],
410+ "idosos" : ["idos" , "envelhec" , "geriatri" ],
411+ "adolescentes" : ["adolesc" , "juven" , "jovem" ],
412+ "infância" : ["infanc" , "crianc" , "infantil" , "pre-escolar" ],
413+ "gênero feminino" : ["mulher" , "feminin" , "genero feminin" ],
414+ "gênero masculino" : ["homem" , "masculin" , "genero masculin" ],
415+ "diversidade" : ["lgbt" , "diversidade" , "genero nao" , "transgener" ],
416+ "contexto clínico" : ["paciente" , "clinic" , "hospital" , "ambulatori" ],
417+ "contexto comunitário" : ["comunitari" , "comunidade" , "atencao primaria" ],
418+ "brasil" : ["brasil" , "latino-americ" , "latino americ" , "latam" ],
419+ },
420+ "dados" : {
421+ "clínicos" : ["escala" , "inventari" , "questionari" , "bdi" , "ham" , "scl" ],
422+ "neurobiológicos" : ["eeg" , "fmri" , "mri" , "neuroimag" , "biomarcador" ],
423+ "qualitativos" : ["entrevista" , "grupo focal" , "discurso" , "narrativa" ],
424+ "observacionais" : ["observac" , "naturalist" , "etnograf" ],
425+ "epidemiológicos" : ["epidemiolog" , "prevalenc" , "incidencia" , "comorbid" ],
426+ "longitudinais" : ["longitudinal" , "follow-up" , "onda" , "wave" , "painel" ],
427+ "comparativos" : ["cross-cultural" , "transcultural" , "cross national" ],
428+ "metadados" : ["meta-analise" , "revisao sistematica" , "metanalise" ],
429+ },
430+ "dominios" : {
431+ "psicologia clínica" : ["psicologi" , "clinic" , "psicopatolog" , "psicoterap" ],
432+ "neurociências" : ["neurocien" , "neurobiolog" , "neuroimag" , "cerebr" ],
433+ "sociologia" : ["sociolog" , "estratificac" , "desiguald" , "capital social" ],
434+ "antropologia" : ["antropolog" , "etnograf" , "cultur" , "ritual" ],
435+ "economia comportamental" : ["economi" , "comportamental" , "nudge" , "vies" ],
436+ "filosofia da mente" : ["filosof" , "conscienc" , "mente" , "fenomenolog" ],
437+ "psicofarmacologia" : ["farmac" , "medicac" , "antidepress" , "psicofarmac" ],
438+ "saúde pública" : ["saude publica" , "sus" , "promocao saude" , "prevenc" ],
439+ "educação" : ["educac" , "ensino" , "aprendizag" , "escolar" ],
440+ "ia tecnologia" : ["inteligencia artificial" , "machine learning" , "deep learning" , "ia" , "chatbot" ],
441+ },
321442 }
322443
323444 # Buscar keywords específicas da dimensão
324445 if dim_key in keyword_map :
325446 for kw_category , keywords in keyword_map [dim_key ].items ():
326447 if kw_category in cat_lower :
448+ # v3.0: word-boundary matching
327449 for kw in keywords :
328- if kw in corpus_lower :
450+ if self . _word_boundary_match ( kw , corpus_lower ) :
329451 return True
330452 return False # Categoria específica não encontrada
331453
332- # Fallback: busca genérica
454+ # Fallback: busca genérica com word-boundary
333455 words = cat_lower .split ()
334- match_count = sum (1 for w in words if len (w ) > 3 and w in corpus_lower )
456+ match_count = 0
457+ for w in words :
458+ if len (w ) > 3 :
459+ if self ._word_boundary_match (w , corpus_lower ):
460+ match_count += 1
335461 return match_count >= len (words ) * 0.5
336462
337463 def _identify_blind_spots_v2 (self , dimension_results : dict [str , Any ]) -> list [dict [str , Any ]]:
@@ -382,7 +508,10 @@ def _generate_recommendations_v2(self, dim_results: dict[str, Any], comfort_zone
382508 return recs if recs else ["Pesquisa com boa cobertura multidimensional." ]
383509
384510 def _identify_blind_spots (self , dimension_results : dict [str , Any ]) -> list [dict [str , Any ]]:
385- """Identifica pontos cegos — dimensões com densidade zero ou muito baixa."""
511+ """@deprecated v1.0 — Substituído por _identify_blind_spots_v2().
512+
513+ Mantido para compatibilidade com código legado.
514+ """
386515 blind_spots = []
387516
388517 for dim_key , dim_data in dimension_results .items ():
@@ -404,7 +533,10 @@ def _generate_recommendations(
404533 dimension_results : dict [str , Any ],
405534 research_domain : str ,
406535 ) -> list [str ]:
407- """Gera recomendações de expansão baseadas nos gaps identificados."""
536+ """@deprecated v1.0 — Substituído por _generate_recommendations_v2().
537+
538+ Mantido para compatibilidade com código legado.
539+ """
408540 recommendations = []
409541
410542 # Recomendações por dimensão com baixa densidade
0 commit comments