Skip to content

Commit 2b701dc

Browse files
author
marce
committed
SPEC-028 v3.0: Noological Scanner refinado — negacao + word-boundary + 5 novas dimensoes — 18/18 CTs (100% pass)
1 parent 6015b2a commit 2b701dc

2 files changed

Lines changed: 290 additions & 35 deletions

File tree

skills/system/academic-audit/noological_scanner.py

Lines changed: 152 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
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)
67
v2.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)
@@ -16,8 +26,8 @@
1626
8. Analise de tendencia (comparacao multi-scan)
1727
1828
Conceito 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

2333
from __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

Comments
 (0)