Files
Investidor-V2/tools/portal-audit-semantic-update.py
nala 01405cbf8a
tests / ci (push) Canceled after 0s
oportunidades 5
2026-10-10 14:15:28 +02:00

121 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reconcile current semantic evidence into the sole audit matrix, preserving every source and version."""
import collections
import csv
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
MATRIX = ROOT / 'docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv'
sys.stdout.reconfigure(encoding='utf-8')
csv.field_size_limit(16 * 1024 * 1024)
http = json.loads((PRIVATE / 'opportunity-semantic-http-qa.json').read_text(encoding='utf-8-sig'))
publication = json.loads((PRIVATE / 'opportunity-semantic-publication-http-qa.json').read_text(encoding='utf-8-sig'))
verified = json.loads((PRIVATE / 'opportunity-semantic-verification-qa.json').read_text(encoding='utf-8-sig'))
environment = json.loads((PRIVATE / 'opportunity-semantic-environment-qa.json').read_text(encoding='utf-8-sig'))
active = json.loads((PRIVATE / 'dashboard-active-readonly.json').read_text(encoding='utf-8-sig'))
assert all(p['active_writes'] == 0 for p in [http, publication, verified, environment, active])
assert http['result'] == publication['result'] == 'passed'
assert http['target'] == publication['target'] == 'desk_restore_20261009_174726_test:55441'
assert sum(c['canonical_validation'] == 'passed' for c in verified['categories']) == 6
rows = list(csv.DictReader(MATRIX.open(encoding='utf-8-sig', newline='')))
before = {r['ID']: dict(r) for r in rows}
assert len(rows) == len(before) == 3386
baseline = PRIVATE / 'opportunity-semantic-matrix-before.csv'
if not baseline.exists():
baseline.write_bytes(MATRIX.read_bytes())
marker = '[Semântica QA 10/10/2026]'
def append(row, field, value):
old = re.sub(re.escape(marker) + r'.*?(?= \| \[|$)', '', row[field]).strip(' |')
row[field] = old + ' | ' + marker + ' ' + value
categories = '; '.join(f"{c['category']}#{c['id']}:{c['working_state']}" for c in verified['categories'])
evidence = ('QA18097:10rascunhos incompletos/nova sessão FortifyMFA;6contratos semânticos submetidos e revistos com gates, '
'5publicações inicialmente internas/DUAT32aprovado internamente+publicação422poratoexternopendente;4rascunhos bloqueados '
'por perfil modular não aprovado. 12erros422porcampo+rascunho inválido preservado. Estado atual:' + categories + '. '
'Investimento30:parecer29atribuído/respondido,aprovadorpendente422/outrodestinatário403/gatesnãoautoaprovados. '
'Venda33:coordenador25→publicador29/fila atribuída→outropublicador403→publicação pública sóteaser;privado preservado. '
'Provas opportunity-semantic-http-qa.json, -publication-http-qa.json e -verification-qa.json; nenhum clique observado.')
implementations = {
'AUD-OP-03': 'Workflow existente reutilizado; filas revisão/editorial/recurso/atribuídas; parecer por gate/hash/destinatário independente; coordenação editorial/reatribuição explícita. Revalida gates e responsabilidades na publicação.',
'AUD-OP-04': 'Adaptador semantic1 e OpportunityCategoryFields detalham os seis contratos do mesmo schema110defs: marcos/cenários, título/serviços/avaliação, comunidades/parcelas/benfeitorias e entidade/financeiros/governação/franquia/NDA/teaser. Fonte/hash/enums antigos preservados; não cria SQL ou identidades paralelos.',
'AUD-OP-02': 'Referências documentais aninhadas pertencem à mesma oportunidade e constam do contrato; originais cifrados/hash/scan verificados. Download/prévia usam VerifiedDocumentBinary e erro controlado; recursos excluídos não são oferecidos no editor.',
'AUD-GEO-03': 'Editor de parcela MultiPolygon com vértices/anéis/fecho explícito; limite administrativo separado. Seleção de localização e campos location_id usam nível mais profundo e mapa/contexto existente. API de seis consumidores recupera limite selecionado e dois limites de contexto; render pendente.',
'AUD-PROFILE-11': 'Validador atribuído e gates por competência; parecer técnico/jurídico exige destinatário independente com âmbito/MFA, resposta exclusiva dele, motivo/evidência e tarefa existente. Parecer não substitui aprovação do gate.',
'AUD-PROFILE-12': 'Publicação atribuível por publication_coordinator; assignee tem âmbito/MFA e é independente da autoria/aprovação. Publicador diverso403; prévia e projeção públicas do negócio usam apenas teaser autorizado.',
'AUD-PROFILE-19': 'Filas por estado/recurso/assignee e nomes de participantes elegíveis no servidor; tasks existentes recebem pareceres/atribuições/revisões/editoriais. SLAunconfigured/due_atnull, sem prazos nem titulares reais inventados.',
'AUD-PROFILE-25': 'Edidores declarativos detalhados pelo contrato do servidor, sem templates divergentes JS; erros canonical.<campo> junto aos controlos e acessibilidade; remoção do formulário núcleo duplicado no modo perfil. País/catálogo/autoridade/documentos selecionáveis; parcela/mapas reutilizados.',
'AUD-PROFILE-28': 'Contratos existentes enriquecidos e versionados sem substituir fonte/artifact110defs; rascunho admite ausência/null e grava/recupera; submissão estrita. Seis categorias demonstradas semanticamente nos cenários documentados; quatro aguardam versões modulares aprovadas.',
}
gaps = {
'AUD-OP-03': 'Visual integral/C37; incidentes/revisão periódica/agendamento; recurso e parecer externo institucionais/SLA versionado. Aprovação interna sintética não comprova atos externos.',
'AUD-OP-04': 'Visual por categoria; correspondência documental/cadastral e urbanística exige evidência/gate humano; não compara automaticamente titular e teor do PDF. Aplicabilidade de unidades/limites/materialidade ainda não expressa por regra inequívoca; Response integral/§27 permanece aberto. Quatro perfis modulares sem decisão.',
'AUD-OP-02': 'Visual de anexos/prévias; cobertura integral DOCX/XLSX/partilhas e PDFs derivados/exportações por perfil ainda sem prova web18097 específica. ClamD real por operação não extrapolado.',
'AUD-GEO-03': 'Render/seletores e mapas visualmente pendentes;393geometriasprivadas=11províncias+128distritos+254postos,188correspondênciaspendentes=31distritos+157postos no pacote581. Localidades/geometrias de parcelas não inferidas desses limites. Nomes mantidos em correspondencias-cartograficas.md.',
'AUD-PROFILE-11': 'Visual e responsabilidade institucional real; parecer externo/protocolos e disponibilidade completos. Gates sintéticos não são decisões jurídicas reais.',
'AUD-PROFILE-12': 'Visual da prévia/imagem/mapa; agendamento/tradução/revisão periódica; DUAT comatoexternopendente não publicável; perfis/regras reais pendentes.',
'AUD-PROFILE-19': 'Visual das filas e reatribuições; decisão exata:qual política versionada define prazos,tolerância,ciclos e escalamento? Quem detém as competências institucionais? Nenhum prazo/responsável real atribuído em QA.',
'AUD-PROFILE-25': 'C37 e navegação/render/erros/seletores/ciclo por conta21 ainda pendentes; código e props não demonstram cliques. Preserva nova sessão e acesso direto às ferramentas.',
'AUD-PROFILE-28': 'Quais versões imutáveis de perfil/campos/documentos/gates são aprovadas para concession,financing,supply,other? Sem inventar contratos. Visual e condições jurídicas/Response integral permanecem parciais.',
}
activation = ('Leitura ativaREADONLY10/10:020000eBalcão07/190000+08/120000instalados;030000não;040000não;'
'09/190000prepare_investor_pathausente(ledger/2colunas/definição). QA03/04/09-190instalados;'
'definição19etapaspublicada61000919-0000-4000-8000-000000000001 SHAe0acbc723cd3def7e415b2ef7a0173add08387c3cedabac9bb7b86409e4d25bf. '
'09/190000nãofoi substituída:continuanecessária paraativo e não se repeteQA. 030/040semdependênciaDDL190;'
'190dependeBalcão/journeys/steps/questionnaire_versions;workflowusa020+030+040+tasks/catalogue. '
'Planofuturo:visual/candidata→backup/restaurofrescos→códigoflagsfalse→030autenticada→040instalador/bootstrapautenticadosaindaemfalta→'
'190apenasfaltantesvia/management/balcao-install→dadosreais/perfis→cartografiaprivadaidempotente→flagspercurso/oportunidades→publicaçãoseparada. '
'0fixturesQAnoativo;readytrueépreparação;semativaçãoautorizada;reversãodesativaflags/preservahistóricoourestaurabackupverificado,nãodropusado.')
for row in rows:
key = row['ID']
if key in implementations:
append(row, 'Implementação encontrada', implementations[key])
append(row, 'Evidência funcional', evidence)
append(row, 'Lacuna', gaps[key])
append(row, 'API', evidence)
append(row, 'Gravação', 'opportunities.management_draft/workflow_context, resources/documents, tasks/audit_events existentes; hashes e histórico. Apenas QA guardada.')
append(row, 'Recuperação', '10categorias/12rascunhos inválidos recuperados noutra sessão MFA; seisoriginais permitidos21/24/25 e negados23; parecer/fila/hash/pub separados da versão privada.')
append(row, 'Interface', 'Código/build exclusivosQA preparados; campo+erro, selector+mapa, filas/pareceres/atribuições. Nenhum clique ou render observado; C37/conta18 e aceitação visual/Promotor21 pendentes.')
append(row, 'Resultado apresentado', 'RespostaHTTP/JSON e persistência demonstrados pelos casos30–39; venda33pública por teaser na prova editorial posterior. Apresentação no navegador pendente.')
append(row, 'Disponibilidade ativa', 'Lote apenasQA18097;ativoREADONLY/flagsfalse,030000/040000/09-190000nãoinstaladas,0geometrias. Sem migração/importação/grants ativos.')
if key != 'AUD-OP-02':
row['Estado'] = 'parcial'
if key in ['AUD-OPS-01', 'AUD-PROFILE-31', 'AUD-DESK-01', 'AUD-GUIDE-01']:
append(row, 'Dependência', activation)
append(row, 'Evidência funcional', 'Provasdeledger/colunas/definição:dashboard-active-readonly.json e opportunity-semantic-environment-qa.json;preservação18coleções/MFA18/manifestoativo:profiles-v1-preservation-qa.json.')
documents_path = PRIVATE / 'opportunity-semantic-documents-http-qa.json'
if documents_path.exists():
doc = json.loads(documents_path.read_text(encoding='utf-8-sig'))
if doc.get('result') == 'passed':
row = next(r for r in rows if r['ID'] == 'AUD-OP-02')
append(row, 'Consolidação', 'PDFválido1pstrictparser+ClamDreal upload/sha/download21/24/25;23download/delete403;delete21download404/removeopçãoeditor;restore+novaMFA21bytes/hashidênticos. Provaopportunity-semantic-documents-http-qa.json. Sem DOCX/XLSX/partilhaderivadainferida.')
protected = ['ID', 'Pedido/trecho', 'Fonte/ocorrências', 'Critério de aceitação', 'Versões divergentes', 'IDs das linhas de origem', 'Quantidade de linhas de origem', 'Critérios e versões por origem (JSON)']
assert all(all(r[k] == before[r['ID']][k] for k in protected) for r in rows)
with MATRIX.open('w', encoding='utf-8-sig', newline='') as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0])); writer.writeheader(); writer.writerows(rows)
origins = [i for r in rows for i in json.loads(r['Critérios e versões por origem (JSON)'])]
assert len(origins) == 6838
states = collections.defaultdict(lambda: {'requisitos': 0, 'linhas_origem': 0})
for r in rows:
states[r['Estado']]['requisitos'] += 1
states[r['Estado']]['linhas_origem'] += int(r['Quantidade de linhas de origem'])
counts_path = PRIVATE / 'contagens-requisitos.json'
counts = json.loads(counts_path.read_text(encoding='utf-8-sig'))
counts.update({'requisitos': len(rows), 'linhas_matriz': len(rows), 'linhas_origem': len(origins), 'por_estado': dict(states),
'semantic_updated_at': datetime.now(timezone.utc).isoformat(), 'browser_current_version_verified': False,
'conservation_sha256': hashlib.sha256(json.dumps(origins, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode()).hexdigest()})
counts_path.write_text(json.dumps(counts, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'requirements': len(rows), 'source_occurrences': len(origins), 'updated': list(implementations), 'active_writes': 0}, ensure_ascii=False))
if (PRIVATE / 'profile-installation-http-qa.json').exists():
import subprocess
subprocess.run([sys.executable, str(ROOT / 'tools/portal-audit-installation-update.py')], check=True)