feat: Adiciona auditor de projetos PSCIP NT-01/2025
Browse files- modules/auditor.py +179 -0
modules/auditor.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Auditor de Projetos PSCIP - NT-01/2025 CBMGO
|
| 3 |
+
Verifica conformidade de memoriais descritivos e projetos.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import re
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Dict, List, Tuple
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AuditorProjetos:
|
| 13 |
+
TERMOS_BUSCA = {
|
| 14 |
+
"extintores_portateis": ["extintor","extintores","po abc","co2","capacidade extintora"],
|
| 15 |
+
"hidrantes": ["hidrante","hidrantes","mangotinho","coluna seca","coluna de incendio","nbr 13714"],
|
| 16 |
+
"sinalizacao_emergencia": ["sinalizacao","sinaliza","emergencia","saida","rota de fuga","nbr 13434"],
|
| 17 |
+
"iluminacao_emergencia": ["iluminacao de emergencia","autonomia","bloco autonomo","nbr 10898"],
|
| 18 |
+
"spda": ["spda","para-raios","descargas atmosfericas","nbr 5419"],
|
| 19 |
+
"alarme_incendio": ["alarme","acionador manual","central de alarme","nbr 17240"],
|
| 20 |
+
"deteccao_automatica": ["detector","detectores","detector de fumaca","deteccao automatica"],
|
| 21 |
+
"chuveiros_automaticos": ["chuveiro","chuveiros","sprinkler","nbr 10897"],
|
| 22 |
+
"saidas_emergencia": ["saida de emergencia","rota de fuga","porta corta-fogo","nbr 9077"],
|
| 23 |
+
"reservatorio_incendio": ["reservatorio","reserva tecnica","cisterna"],
|
| 24 |
+
"plano_emergencia": ["plano de emergencia","brigada","simulacro","nbr 15219"],
|
| 25 |
+
"ventilacao_mecanica": ["ventilacao","exaustao","renovacao de ar"],
|
| 26 |
+
}
|
| 27 |
+
SEVERIDADE = {
|
| 28 |
+
"extintores_portateis": "CRITICO","hidrantes": "CRITICO",
|
| 29 |
+
"sinalizacao_emergencia": "CRITICO","iluminacao_emergencia": "IMPORTANTE",
|
| 30 |
+
"spda": "IMPORTANTE","alarme_incendio": "IMPORTANTE",
|
| 31 |
+
"deteccao_automatica": "IMPORTANTE","chuveiros_automaticos": "CRITICO",
|
| 32 |
+
"saidas_emergencia": "CRITICO","reservatorio_incendio": "IMPORTANTE",
|
| 33 |
+
"plano_emergencia": "ALERTA","ventilacao_mecanica": "ALERTA",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
def __init__(self, regras_path="data/regras_declarativas.json"):
|
| 37 |
+
self.regras = []
|
| 38 |
+
if os.path.exists(regras_path):
|
| 39 |
+
with open(regras_path, "r", encoding="utf-8") as f:
|
| 40 |
+
self.regras = json.load(f)
|
| 41 |
+
|
| 42 |
+
def _aplicar_regras(self, dados):
|
| 43 |
+
exigencias = {"extintores_portateis", "sinalizacao_emergencia"}
|
| 44 |
+
area = dados.get("area_m2", 0)
|
| 45 |
+
altura = dados.get("altura_m", 0)
|
| 46 |
+
ocupacao = dados.get("ocupacao", "").lower()
|
| 47 |
+
lotacao = dados.get("lotacao", 0)
|
| 48 |
+
|
| 49 |
+
if area > 750 or ocupacao in ["comercial","servicos","industrial"]:
|
| 50 |
+
exigencias.add("hidrantes")
|
| 51 |
+
if altura > 12:
|
| 52 |
+
exigencias.update(["iluminacao_emergencia","saidas_emergencia"])
|
| 53 |
+
if area > 1000:
|
| 54 |
+
exigencias.add("spda")
|
| 55 |
+
if lotacao > 100 or area > 750:
|
| 56 |
+
exigencias.add("alarme_incendio")
|
| 57 |
+
if lotacao > 200:
|
| 58 |
+
exigencias.add("deteccao_automatica")
|
| 59 |
+
if (ocupacao == "industrial" and area > 2000) or altura > 30:
|
| 60 |
+
exigencias.add("chuveiros_automaticos")
|
| 61 |
+
if any(u in ocupacao for u in ["hospital","saude"]):
|
| 62 |
+
exigencias.update(["chuveiros_automaticos","deteccao_automatica",
|
| 63 |
+
"alarme_incendio","plano_emergencia"])
|
| 64 |
+
if "garagem" in ocupacao or "estacionamento" in ocupacao:
|
| 65 |
+
exigencias.add("ventilacao_mecanica")
|
| 66 |
+
|
| 67 |
+
for regra in self.regras:
|
| 68 |
+
match = True
|
| 69 |
+
for k, v in regra.get("se", {}).items():
|
| 70 |
+
if k == "ocupacao":
|
| 71 |
+
if dados.get("ocupacao","").lower() != str(v).lower():
|
| 72 |
+
match = False; break
|
| 73 |
+
elif isinstance(v, str) and v.startswith(">"):
|
| 74 |
+
if dados.get(k, 0) <= float(v[1:]):
|
| 75 |
+
match = False; break
|
| 76 |
+
elif isinstance(v, str) and v.startswith("<"):
|
| 77 |
+
if dados.get(k, 0) >= float(v[1:]):
|
| 78 |
+
match = False; break
|
| 79 |
+
elif str(dados.get(k,"")) != str(v):
|
| 80 |
+
match = False; break
|
| 81 |
+
if match:
|
| 82 |
+
for e in regra.get("entao", []):
|
| 83 |
+
exigencias.add(e)
|
| 84 |
+
|
| 85 |
+
return list(exigencias)
|
| 86 |
+
|
| 87 |
+
def auditar_memorial(self, memorial_txt, dados_projeto):
|
| 88 |
+
if not memorial_txt or len(memorial_txt) < 50:
|
| 89 |
+
return {"status":"INVALIDO","apto":False,"erros":[],"alertas":[],"conformes":[],"score":0}
|
| 90 |
+
|
| 91 |
+
ml = memorial_txt.lower()
|
| 92 |
+
exigencias = self._aplicar_regras(dados_projeto)
|
| 93 |
+
erros, alertas, conformes = [], [], []
|
| 94 |
+
|
| 95 |
+
for exig in exigencias:
|
| 96 |
+
termos = self.TERMOS_BUSCA.get(exig, [exig.replace("_"," ")])
|
| 97 |
+
enc = [t for t in termos if t in ml]
|
| 98 |
+
sev = self.SEVERIDADE.get(exig, "ALERTA")
|
| 99 |
+
item = {"exigencia": exig, "descricao": exig.replace("_"," ").title(),
|
| 100 |
+
"severidade": sev, "termos_encontrados": enc}
|
| 101 |
+
if enc:
|
| 102 |
+
conformes.append(item)
|
| 103 |
+
else:
|
| 104 |
+
item["mensagem"] = f"{item['descricao']} - nao encontrado"
|
| 105 |
+
(erros if sev == "CRITICO" else alertas).append(item)
|
| 106 |
+
|
| 107 |
+
# Alertas de dados tecnicos
|
| 108 |
+
if not re.search(r'\d+\s*m[2²]', memorial_txt, re.IGNORECASE):
|
| 109 |
+
alertas.append({"tipo":"DADO","mensagem":"Area em m2 nao encontrada","severidade":"ALERTA"})
|
| 110 |
+
if not re.search(r'(responsavel tecnico|crea|cau)', ml):
|
| 111 |
+
alertas.append({"tipo":"RT","mensagem":"Responsavel Tecnico nao identificado","severidade":"IMPORTANTE"})
|
| 112 |
+
if "nt-01" not in ml and "nt 01" not in ml:
|
| 113 |
+
alertas.append({"tipo":"REF","mensagem":"Referencia NT-01/2025 nao encontrada","severidade":"ALERTA"})
|
| 114 |
+
|
| 115 |
+
total = len(exigencias)
|
| 116 |
+
score = round(len(conformes) / total * 100 if total > 0 else 0, 1)
|
| 117 |
+
apto = len(erros) == 0
|
| 118 |
+
|
| 119 |
+
return {
|
| 120 |
+
"status": "APTO PARA PROTOCOLO" if apto else "PENDENCIAS ENCONTRADAS",
|
| 121 |
+
"score_conformidade": score, "apto": apto,
|
| 122 |
+
"total_exigencias": total, "conformes": conformes,
|
| 123 |
+
"erros": erros, "alertas": alertas,
|
| 124 |
+
"data_auditoria": datetime.now().isoformat(),
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
def formatar_relatorio(self, resultado):
|
| 128 |
+
status = resultado.get("status","?")
|
| 129 |
+
score = resultado.get("score_conformidade", 0)
|
| 130 |
+
erros = resultado.get("erros", [])
|
| 131 |
+
alertas = resultado.get("alertas", [])
|
| 132 |
+
conformes = resultado.get("conformes", [])
|
| 133 |
+
|
| 134 |
+
linhas = [
|
| 135 |
+
"=" * 60,
|
| 136 |
+
"RELATORIO DE AUDITORIA - PSCIP",
|
| 137 |
+
f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M')}",
|
| 138 |
+
f"STATUS: {status}",
|
| 139 |
+
f"Score de conformidade: {score}%",
|
| 140 |
+
"=" * 60,
|
| 141 |
+
]
|
| 142 |
+
if conformes:
|
| 143 |
+
linhas.append(f"\nITENS CONFORMES ({len(conformes)}):")
|
| 144 |
+
for c in conformes:
|
| 145 |
+
linhas.append(f" [OK] {c['descricao']}")
|
| 146 |
+
if erros:
|
| 147 |
+
linhas.append(f"\nERROS CRITICOS ({len(erros)}) - IMPEDEM PROTOCOLO:")
|
| 148 |
+
for e in erros:
|
| 149 |
+
linhas.append(f" [!!] {e['descricao']} - INCLUIR NO MEMORIAL")
|
| 150 |
+
if alertas:
|
| 151 |
+
linhas.append(f"\nALERTAS ({len(alertas)}):")
|
| 152 |
+
for a in alertas:
|
| 153 |
+
linhas.append(f" [{a.get('severidade','?')}] {a.get('mensagem',a.get('descricao',''))}")
|
| 154 |
+
linhas.extend(["", "Ref: NT-01/2025 CBMGO", "=" * 60])
|
| 155 |
+
return "\n".join(linhas)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
_auditor_instance = None
|
| 159 |
+
|
| 160 |
+
def get_auditor():
|
| 161 |
+
global _auditor_instance
|
| 162 |
+
if _auditor_instance is None:
|
| 163 |
+
_auditor_instance = AuditorProjetos()
|
| 164 |
+
return _auditor_instance
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
auditor = AuditorProjetos()
|
| 169 |
+
memorial = """MEMORIAL DESCRITIVO - PSCIP
|
| 170 |
+
Edificacao comercial 1500m2, 2 pavimentos.
|
| 171 |
+
Extintores portateis: 8 un, 2-A:20-B:C, NT-01/2025.
|
| 172 |
+
Hidrantes: coluna seca, 300 L/min, NBR 13714.
|
| 173 |
+
Sinalizacao de emergencia: NBR 13434.
|
| 174 |
+
SPDA: obrigatorio area > 1000m2, NBR 5419.
|
| 175 |
+
Responsavel Tecnico: Eng. Silva CREA 123456
|
| 176 |
+
"""
|
| 177 |
+
dados = {"area_m2": 1500, "altura_m": 7, "ocupacao": "comercial", "lotacao": 100}
|
| 178 |
+
res = auditor.auditar_memorial(memorial, dados)
|
| 179 |
+
print(auditor.formatar_relatorio(res))
|