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

376 lines
37 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 both profile specifications in the existing matrix; preserve every source occurrence."""
import collections
import csv
import hashlib
import json
import re
import shutil
import subprocess
import sys
import zipfile
from datetime import datetime, timezone
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
csv.field_size_limit(16 * 1024 * 1024)
ROOT = Path(__file__).resolve().parent.parent
DOCS = ROOT / 'docs/investmoz/v2/auditoria-portal'
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
SOURCE = Path('C:/Mac/Home/Documents/TEMPO/Portal Tempo/Servicos Tempo')
MATRIX = DOCS / 'matriz-requisitos.csv'
rows = list(csv.DictReader(MATRIX.open(encoding='utf-8-sig', newline='')))
fields = list(rows[0])
baseline = PRIVATE / 'profiles-v1-matrix-before.csv'
if not baseline.exists():
shutil.copyfile(MATRIX, baseline)
before = list(csv.DictReader(baseline.open(encoding='utf-8-sig', newline='')))
assert len(before) == 3354
assert sum(int(r['Quantidade de linhas de origem']) for r in before) == 5830
# Reconcile the full accessible corpus, including embedded original media/attachments.
inventory_path = DOCS / 'inventario-fontes.csv'
inventory = list(csv.DictReader(inventory_path.open(encoding='utf-8-sig', newline='')))
inventory_fields = list(inventory[0])
seen = {(r['Exemplar/caminho'], r['SHA256']) for r in inventory}
paths = subprocess.check_output(['rg', '--files', '--hidden', '--no-ignore', str(SOURCE)], text=True, encoding='utf-8').splitlines()
errors = []
new_inventory = []
def inventory_entry(name, data, kind):
digest = hashlib.sha256(data).hexdigest()
if (name, digest) in seen:
return
seen.add((name, digest))
new_inventory.append(dict(zip(inventory_fields, [name, str(len(data)), digest, kind,
'Inventário complementar 10/10/2026; versões anteriores preservadas',
'Acessível localmente; importação/publicação não inferida', 'Nenhum cálculo ativado',
'Situação jurídica/documental não inferida do inventário', 'Exemplar identificado por SHA256'])))
for filename in paths:
path = Path(filename)
try:
raw = path.read_bytes()
inventory_entry(str(path), raw, 'original')
if not path.name.startswith('~$') and path.suffix.lower() in {'.docx', '.xlsx', '.pptx', '.docm', '.xlsm', '.pptm'}:
with zipfile.ZipFile(path) as archive:
for member in archive.namelist():
if '/media/' in member or '/embeddings/' in member:
inventory_entry(str(path) + '#' + member, archive.read(member), 'anexo embebido')
except (OSError, zipfile.BadZipFile) as error:
errors.append({'path': str(path), 'error_type': type(error).__name__})
inventory.extend(new_inventory)
with inventory_path.open('w', encoding='utf-8-sig', newline='') as handle:
writer = csv.DictWriter(handle, fieldnames=inventory_fields); writer.writeheader(); writer.writerows(inventory)
specs = [
('PROFILEV1', 'Gestao de perfis investeMOZ v1.docx', 'profiles-v1-source.txt', 'f9e0f6280566fed8b52d0b36bb814a82e11976fec9ea9f62d64611ff8c824b38', 530),
('PROFILEOLD', 'Gestao de perfis investeMOZ.docx', 'profiles-prior-source.txt', '283a32f4c4b02bdfd9318a9174268224eeefac4349c49e3171545707bd222855', 471),
]
sources = []
for prefix, name, extract, digest, total in specs:
path = SOURCE / 'prompt/novos' / name
raw = path.read_bytes()
assert hashlib.sha256(raw).hexdigest() == digest, 'Source changed; preserve and reconcile a new version before updating.'
original_dir = PRIVATE / 'profile-source-originals'; original_dir.mkdir(exist_ok=True)
exemplar = original_dir / (digest + '.docx')
if not exemplar.exists(): exemplar.write_bytes(raw)
lines = (PRIVATE / extract).read_text(encoding='utf-8').splitlines()
parsed = [re.match(r'^(\d+): ?(.*)$', line) for line in lines]
parsed = [m for m in parsed if m]
assert len(parsed) == total
sources.append({'prefix': prefix, 'name': name, 'path': str(path), 'sha256': digest,
'paragraphs': [(int(m[1]), m[2]) for m in parsed], 'original_preserved': str(exemplar), 'read_integrally': True})
# One verifiable requirement per entry. Full source paragraphs are references, not new requirements.
requirements = [
(1, 'Seis perfis sobre identidades existentes, com RBAC e ABAC', 'parcial',
'PortalProfileAccess; users/Fortify, organizations/memberships, quatro atribuições operacionais e dupla controlada; público derivado de conta verificada.',
'Doze atores normais MFA e consultas por perfil no servidor QA; schema ligado às entidades canónicas.',
'Âmbito provincial ainda não modelado; migração reconciliada dos gestores legados e aceitação visual por perfil.',
'Cada perfil corresponde a uma identidade existente e a regras de servidor; nenhum cadastro de identidades duplicado.'),
(2, 'Utilizador público: consulta, favoritos/alertas e pedidos próprios', 'parcial',
'Capacidades públicas existentes; pedido próprio de Promotor reutiliza conta verificada.',
'Conta20: dashboard→pedido, criação403 e consulta de pedido alheio403; favoritos/alertas não ensaiados neste lote.',
'Demonstrar favoritos/alertas e todos os pedidos pessoais na interface; alternativa de confirmação por telefone da v1 ainda não implementada, verificação de email existente preservada.',
'Consulta e pedidos próprios funcionam; criação/edição/decisão de oportunidades são negadas ao público.'),
(3, 'Solicitar perfil de Promotor com representação, tipos e comprovativos', 'parcial',
'PortalProfileAdministration::request; documentos canónicos próprios/hash/scan, chave idempotente, âmbito individual ou organização representada.',
'Contas21/22/23/31: quatro pedidos, replay sem duplicação, recuperação noutra sessão, quatro comprovativos sintéticos com ClamD real e recuperação por hash.',
'Verificação visual do formulário, submissão de tipos adicionais e regras institucionais de comprovativos por representação.',
'Pedido guarda representação/tipos/comprovativos, fica recuperável e não concede o papel por si só.'),
(4, 'Decisão independente do pedido de Promotor e tipos aprovados', 'parcial',
'Fila administrativa, consulta privada dos comprovativos por VerifiedDocumentBinary, versão e atestações auditadas; sem autoaprovação.',
'Super27 consulta originais por hash e aprova quatro pedidos de outros atores; teste nega decisão própria e regista rejeição com verificações falsas sem inventar confirmação.',
'Aceitação visual da fila e decisões; não foram validadas identidades reais, apenas documentos sintéticos QA.',
'Somente aprovador distinto com MFA confirma identidade/representação/documentos e concede os tipos efetivamente pedidos e aprovados.'),
(5, 'Navegação por perfil a partir do dashboard', 'parcial',
'ProfileNavigation e props de autorização partilhadas; páginas existentes reutilizadas em /minhas-oportunidades.',
'DashboardHTTP21 devolve Minhas oportunidades/Nova oportunidade;20 devolve Solicitar perfil; ensaio segue os URLs devolvidos, sem depender de rota conhecida previamente.',
'Cliques, renderização, responsividade e resultado visível no navegador; C37 não fechado.',
'Promotor autorizado descobre lista/criação no dashboard; os outros descobrem pedido e acompanhamento sem obter criação.'),
(6, 'Edição organizacional por outros Promotores e âmbito pessoal exclusivo', 'parcial',
'OpportunityManagement/Resources reutilizados; owner_user_id preservado, contributors e membership de escrita em servidor.',
'Autor21 cria oportunidade15; colega22 autorizado edita, recuperação mantémID/autor;23 fora da organização403. Seletores/mapas mudam para endpointsautorizadosv1:11províncias, seleçãoCaboDelgado gravada/recuperada noutra sessão com geometriaAPI. Teste de representação revogada nega escrita.',
'Percurso visual e coedição dos recursos por operação; restrições provinciais permanecem separadas.',
'Outros Promotores autorizados da organização editam rascunho/correção; pessoal sem organização só pelo titular; histórico preservado.'),
(7, 'Autorizações por tipo e representação sem transferência silenciosa', 'parcial',
'Tipos em atribuições e pedidos; organização imutável na edição, autor/workflow/status proibidos no payload.',
'HTTP22: mudança para tipo não aprovado403 e transferência de organização403; fixture testa isolamento de tipos.',
'Demonstrar todos os contratos especializados e seus campos/validadores; seleção do tipo não os encerra.',
'Servidor nega qualquer criação/edição fora dos tipos aprovados ou da representação; novos tipos exigem decisão.'),
(8, 'Estados, razões, versões e dependências reais do workflow', 'parcial',
'OpportunityProfileWorkflow reutiliza códigos/management_version/etag/audit_events; revisão, correção, parecer, rejeição/suspensão/arquivo e pedidos segregados.',
'HTTP: pedido de arquivo guarda e mantémdraft, submissão422 com lacuna explícita; fixture submetida inicia revisão, pede correção e recupera edição.',
'Submissão completa, aprovação/publicação, incidentes e conservação de versão pública ainda não demonstradas; fixture não é submissão real.',
'Transições válidas e fundamentadas preservam versão/história; inválidas são negadas; preparação não se confunde com decisão externa.'),
(9, 'Impedir aprovação própria, conflitos de organização e publicação pelo aprovador', 'parcial',
'Autor, contributors e membership da organização impedem revisão; publicação verifica aprovador da mesma versão; autorizações rechecadas sob lock.',
'Teste autor com Promotor+Validador+Publicador continua403; regras editor/representação verificadas no serviço.',
'Publicação real permanece bloqueada pelo contrato; testar ciclo completo e reatribuição independente com versão aprovada real.',
'Autor/editor/representante não aprova; aprovador não publica a mesma versão; elevação não remove esses impedimentos.'),
(10, 'Acumulação de papéis com negação prevalecente', 'parcial',
'Auditor nega escrita operacional; Super exige papel operacional mais elevação independente; papéis não se somam para ultrapassar restrições.',
'Conta31 Promotor+Validador+Auditor lê e recebe403 na edição;27 Super sem Promotor/elevação recebe403 na criação. Fixture com papéis v1 e allowlist legada: criação fora do âmbito403, dashboard sem criação legada; Auditor nega escrita administrativa. Revogação/expiração não restaura permissão legada automaticamente.',
'Demonstração visual e combinações operacionais adicionais com elevação aprovada.',
'A acumulação nunca autoriza ato negado por autoria, organização, tipo, estado, independência ou Auditor.'),
(11, 'Validador: revisão atribuída, MFA, correções e decisão', 'parcial',
'Papel validator com âmbito/tipos e responsabilidades compatíveis; MFA confirmado exigido na atribuição e recente na operação.',
'24 aprovado inicialmente, aprovação de draft409, revogado depois; fixture independente revisa/corrige e recupera nova versão; semMFA403.',
'Fila de atribuição e completude normativa, decisão positiva integral e interface.',
'Validador independente e com MFA só decide âmbito/tipo/estado permitidos; justificação e itens de correção persistem.'),
(12, 'Publicador: versão aprovada, suspensão/arquivo e segregação', 'parcial',
'Papel publisher, MFA recente, independência; conteúdo técnico não editável; publicar bloqueado se completude/versão faltam.',
'25 publica draft409; fixture confirma estado incompatível; todos os atos positivos de publicação integral continuam pendentes.',
'Publicação e agendamento, revisão de divulgação, suspensão urgente/retificação e prova da versão pública.',
'Publicar somente versão completa aprovada por outro ator; suspender/arquivar com razão sem alterar original nem decisão externa.'),
(13, 'Auditor: consulta filtrada/exportação e ausência de escrita', 'parcial',
'/perfis/auditoria usa audit_events existentes, filtros e JSON minimizado; Auditor conserva impedimento de escrita.',
'26 recupera substituição filtrada e exporta metadados sem snapshots privados;20 recebe403;31 multirrole edição403.',
'Interface, exportação ampliada autorizada por classificação e consulta dos restantes domínios administrativos.',
'Filtros retornam só metadados autorizados e exportação rastreável; nenhum botão/API permite escrita operacional ao Auditor.'),
(14, 'Superutilizador: administração controlada e elevação temporária', 'parcial',
'PortalSuperusers/Administration; MFA recente, motivo, aprovação independente e validade15min; aprovador deve continuar na dupla.',
'27 administra pedidos/papéis, autoaprovação403, criação sem papel403; elevação API preparada, ainda sem ato operacional positivo completo.',
'Demonstração da elevação completa, expiração, scope/action exatos e interface de administração.',
'Super administra sem operar automaticamente; elevação independente expira e não substitui papel operacional ou segregação.'),
(15, 'Exatamente dois Superutilizadores: inicialização e substituição atómica', 'parcial',
'Dupla singleton ligada a users, CHECK distintos, triggerDELETE/TRUNCATE, versão/lock, aprovação e atualização única.',
'Inicialização27/28 pelo29; terceira atribuição422; substituição28→30 com aprovação28 independente de27, dupla27/30; replay409, sessão28invalidada. TriggerDELETE testado.',
'Interface, testes concorrentes em sessões simultâneas e cerimónia ativa com titulares reais ainda não designados.',
'Após inicialização existem sempre dois titulares distintos; nem terceiro, remoção isolada, autoaprovação ou replay altera essa cardinalidade.'),
(16, 'Recuperar acesso preservando MFA e dupla', 'parcial',
'Códigos de recuperação Fortify e substituição independente; bloqueio de desativaçãoMFA, mudançaemail/eliminação de titular.',
'30 recupera sessãoMFA noutra sessão pelo códigoFortify; reutilização422 sem autenticação; DELETE-MFA/conta e PATCHemail403.',
'Procedimento institucional quando ambos perdem todos os meios; aceitação visual e guarda local dos códigos sem os transmitir.',
'Recuperação normal mantém MFA e dois titulares; código consumido não autentica; emergência não é desativaçãoMFA ou key:generate.'),
(17, 'Revogação imediata com histórico e invalidação de sessões/tokens', 'parcial',
'Atribuição retida, revision de autorização por conta, remember_token revogado; adaptação sessions/personal_access_tokens quando presentes.',
'24 antigo cookie401; nova sessãoMFA autentica mas acesso de papel403;28 substituído igual. Histórico original preservado.',
'Tokens pessoais não instalados/ensaiados no alvo, dispositivos/integradores e interface por consumidor.',
'Revogar invalida sessões e credenciais de acesso aplicáveis sem apagar conta, MFA, documentos, autoria ou decisões.'),
(18, 'Validade, reavaliação por inatividade e documentação expirada', 'parcial',
'starts_at/expires_at/revoked_at consultados a cada pedido; atribuições documentadas sem cache de autorização.',
'Fixture de papel expirado não autoriza; MFA antigo não permite decisão.',
'Revalidação/inatividade/expiração documental, parâmetros aprovados e avisos; não inventar prazos.',
'Atribuição expirada ou representação revogada deixa de autorizar; reavaliação usa prazos/regra versionados aprovados.'),
(19, 'Coordenadores de validação/publicação e pareceres técnico-jurídicos', 'não implementado',
'Responsabilidades mapeadas e compatibilidade por papel validadas; entidades existentes de tarefa/auditoria a reutilizar.',
'Responsabilidade validation_coordinator em Auditor422; papéis/responsabilidades armazenáveis, filas e reatribuições não implementadas.',
'Implementar filas/atribuição, conflito, disponibilidade, parecer e escalamento sem novos perfis ou identidades.',
'Coordenador autorizado atribui/reencaminha; parecerista responde; impedimentos persistem; ação visível e recuperável.'),
(20, 'Suporte técnico, incidentes e preservação da submissão', 'não implementado',
'Balcão/tarefas/audit_events existentes; suporte mapeado sem acesso documental automático; nenhum gestor fictício.',
'Correspondência documental preparada; protocolo de incidentes da submissão não implementado neste workflow.',
'Incidentes recuperáveis, suporte atribuído, falha de submissão/ficheiro sem perda, alertas e estados próprios.',
'Falha técnica conserva submissão/versão, abre incidente e encaminha sem dar ao suporte permissão de decisão ou dados indevidos.'),
(21, 'Recurso com instância independente e conservação da decisão original', 'parcial',
'Responsabilidade appeal_reviewer de Validador e pedido de recurso no workflow_context existente.',
'API de pedido prevista apenas rejected; decisão independente e percurso positivo de recurso não demonstrados.',
'Fila/revisor distinto do decisor inicial, resposta/motivo/versionamento e recuperação visual.',
'Recurso não sobrescreve decisão; revisor distinto e autorizado decide com razão, sem publicar.'),
(22, 'Prazos, tolerâncias e ciclos aprovados/configuráveis/versionados', 'parcial',
'policy_version nas atribuições e workflow, correction_cycles preservados; ausência de parâmetros não encerra processos automaticamente.',
'Versão de autorização profiles-v1-20261010 e correção auditadas; gestão/efeito de parâmetros ainda não implementados.',
'Tela/versionamento/governança e adaptação dos processos antigos; valores institucionais aprovados.',
'Alterar SLA/limites requer aprovação, vigência e versão; processo antigo conserva regra aplicável e nenhuma configuração inventa prazo.'),
(23, 'Auditoria integral, razões, origem e alertas de segurança', 'parcial',
'audit_events existente com ator/IP/tempo/correlação/política/razão e before-after relevantes; denegação sanitizada sem corpoauth.',
'HTTPs e fixture recuperam decisão/revogação/substituição e alerta administrativo; exportação minimizada não inclui segredos.',
'Cobertura dos fluxos ainda ausentes, exportação autorizada auditada, alertas operacionais e interface.',
'Cada decisão/crítica/negação guarda contexto rastreável e antes/depois aplicável; leitura/exportação não expõe segredos.'),
(24, 'MFA e segurança de sessão/autenticação sem contornar C37', 'parcial',
'Fortify existente, MFA normal, sessão QA/cookie isolados e epochs; proteção de MFA obrigatório em papéis críticos.',
'Doze contas confirmamMFA pelo servidor18097, nova sessão e recuperação30 comprovadas; C37conta18 não repetido/fechado.',
'C37/interface, duração/reautenticação operacional e restantes consumidores/devices.',
'Login→MFA→sessão→reload→logout→novo login funciona visualmente sem partilha indevida de cookie ou desativaçãoMFA.'),
(25, 'Interface responsiva, gravação segura, erros e acompanhamento', 'parcial',
'Formulários/feedback existentes reutilizados; novas páginas de pedido/admin/auditoria e contexto organizacional.',
'PropsHTTP/compilação e TypeScript verificáveis; gravação/versionamentoAPI, nenhum navegador automatizável inventariado.',
'Demonstração visual/acessibilidade, autosave e erros por campo nos contratos completos.',
'Formulários recuperam dados, mostram ações/erros/pendências e não perdem resposta; navegação acessível por perfil.'),
(26, 'Privacidade, consentimento, consulta/correção/desativação', 'parcial',
'users/consents/documentos privados existentes; autorizações por pedido; remoção que destruiria titulares/histórico é negada.',
'Comprovativos privados por hash/autorização e não publicados; proteção de titular403; fluxo geral de desativação não ensaiado.',
'Consulta/correção/desativação preservando rastreabilidade e direitos aplicáveis, retenção/documentação por tipo.',
'Titular gere dados/consentimento sem perder história ou eliminar dupla; dados privados não passam ao público.'),
(27, 'Avaliar Keycloak antes de substituir Fortify', 'concluído e demonstrado',
'Decisão arquitetural explícita em perfis-v1-reconciliacao.md; Fortify mantido e autorização funcional na aplicação.',
'Código/schema confirmam Fortify e ausência de Keycloak; documentação oficial Keycloak/Laravel confrontada; necessidadeSSO/federação não demonstrada.',
'Nenhuma lacuna desta avaliação restrita; eventual adoção futura exige necessidade/IDs/MFA/recuperação/migração aprovados.',
'Recomendação avaliada; decisão fundamentada preserva autenticação/IDs; não declara integraçãoKeycloak ativa.'),
(28, 'Contratos especializados completos antes de submissão/aprovação/publicação', 'parcial',
'Schema normativo110defs/novecontratos e primeiro draftimobiliário anteriores; novos gates falham de forma controlada.',
'Submissão realdraft15 responde422 com lacuna do contratoCreate; versões públicas não alteradas.',
'Adaptadores canónicos, camposUI/regras/vínculos/evidências especializados e ciclo completo sem fixtureestado.',
'Uma oportunidade completa cumpre contrato/tipo/vínculos; submissão/revisão/aprovação/publicação integral por atores distintos é demonstrada.'),
(29, 'Reconciliar permissões administrativas legadas sem retirada automática', 'dependente de decisão concreta',
'manage-portal preservado para registroslegados; novos privados não autorizados pela allowlist; nenhuma conta ativa promovida/revogada.',
'AtivoREADONLY:rolesv1 ausentes/flagfalse; gestores2/13 existentes; fixture nega acesso legado ao novo privado sem perfil.',
'Pergunta para futuraativação: que funções dos gestores ativos devem corresponder a Validador, Publicador, Auditor ou administração controlada? Quem serão os dois titulares e o aprovador da cerimónia?',
'Correspondência aprovada conserva contas/história e deixa diferenças/permissões explícitas; nenhuma revogação ativa automática.'),
(30, 'Contas sintéticas por perfil e demonstração permitida/negada', 'parcial',
'Doze contas sintéticasQA, duas organizações e quatro representações; contas antigas preservadas.',
'profiles-v1-http-qa.json e recovery-audit: permitido/negado, rolesmúltiplos, troca/revogação/nova sessão sem divulgarcredenciais.',
'Demonstração visual por perfil;24 agora revogado e28 substituído, não apresentar esses atores como papéis ainda ativos.',
'Cada perfil tem ensaio positivo/negativo e recuperação noutra sessão; revogação/múltiplos papéis não contornam autorização.'),
(31, 'Instalação, disponibilização e reversão dos perfis separadas', 'parcial',
'Migration040000guardada, flagQAtrue/ativofalse, fingerprints das instalações existentes incluem o código de autorização.',
'QA:4tabelas,users1/opportunities3colunas,1função2triggers; ativoREADONLYausente. Baseline18tabelas originais preservadas; assetsativosSHAigual.',
'Mecanismo autenticado de instalação040000 e bootstrap ativo auditável, nova prova backup/restauro/compatibilidade/reversão apósvisuais, ativação não autorizada.',
'Instalar somente ausentes com prévia/provas/replay; designar dupla de forma controlada; flag não é instalação; rollback conserva dados e nega acesso aos privados.'),
(32, 'Arquivo/exclusão/restauro do Promotor versus gestor legado', 'parcial',
'Decisão antes do código:Promotorv1 apenas solicita; Publicador decidearquivo; lifecyclegestorlegado preservado para antigos, privado não herda.',
'HTTP21pedidoarquivo persiste sem mudar draft; endpointlifecycle privado403; gestorC43 anterior conserva percurso independente.',
'Interfacepedido/decisãopublicador com caso válido e critério de conservação; não prometer exclusão/restauro não concedidos pela v1.',
'Promotor solicita com razão, sem arquivar/apagar/restaurar; decisor autorizado registraresultado; legado mantém direitos identificados.'),
]
definition = {n: {'title': title, 'state': state, 'implementation': impl, 'evidence': evidence, 'gap': gap, 'acceptance': acceptance}
for n, title, state, impl, evidence, gap, acceptance in requirements}
def classify(number, text):
# Tests are traced to their governing requirement. Shared structural/table paragraphs remain in the model reference.
if 397 <= number <= 411: return 14 if number < 407 else 17
if 412 <= number <= 426: return 2
if 427 <= number <= 431: return 28
if 432 <= number <= 436: return 7
if 437 <= number <= 441: return 6
if 442 <= number <= 446: return 11
if 447 <= number <= 466: return 9 if number >= 457 and number <= 461 else 11
if 467 <= number <= 486: return 12
if 487 <= number <= 501: return 13
if 508 <= number <= 517: return 15
if 524 <= number <= 528: return 22
specific = {33:1,42:15,265:15,266:15,267:15,268:15,269:15,332:2,333:3,334:4,335:7,336:18,337:22,
339:10,340:6,341:28,342:8,343:8,344:10,345:14,346:23,348:24,349:7,350:24,351:1,352:25,353:26,355:1,357:27}
if number in specific: return specific[number]
value = text.lower()
for terms, requirement in [(['keycloak','openid','oauth'],27), (['substitui','exatamente dois','menos de dois','terceiro super'],15),
(['recupera','bloqueio de acesso'],16), (['revoga','invalidar sessões'],17), (['inatividade','expir'],18),
(['instância de recurso','recurso contra','revisor de recurso'],21), (['suporte','incidente','falha técnica'],20),
(['coordenador','parecer técnico','parecer jurídico','reencaminh'],19), (['sla','tolerância','limites de ciclos','prazos'],22),
(['autosave','responsiv','acessibil'],25), (['privacidade','consentimento','dados pessoais'],26),
(['segregação','própria','conflito de interesse'],9), (['acumula','negação'],10), (['elevação','ações operacionais globais'],14),
(['solicitar perfil','comprovativos','pedido de perfil'],3), (['mfa','multifator','sessões'],24),
(['arquivo','exclusão','restauro'],32), (['auditor','exporta'],13), (['auditoria','rastreabilidade'],23),
(['organização','representação','propriedade'],6), (['tipos de oportunidade','tipo autorizado'],7),
(['aprovação','validar','validador','correções'],11), (['publicador','publicar','publicação'],12),
(['estados','rascunho','submiss'],8), (['menu','navegação','dashboard'],5), (['público','favoritos','alertas'],2)]:
if any(term in value for term in terms): return requirement
return 1
assigned = collections.defaultdict(list)
v1_exact = {text: classify(number, text) for number, text in sources[0]['paragraphs'] if text}
for source in sources:
for number, text in source['paragraphs']:
requirement = classify(number, text) if source['prefix'] == 'PROFILEV1' else v1_exact.get(text, classify(0, text))
assigned[requirement].append({'id': f"{source['prefix']}-P{number:04d}", 'pedido': text,
'fontes': [f"{source['name']} | parágrafo {number} | SHA256 {source['sha256']}"],
'aceitacao': definition[requirement]['acceptance'], 'estado': definition[requirement]['state'],
'variantes': 'Versão1.0 ou exemplar anterior preservados integralmente; divergências em perfis-v1-reconciliacao.md.', 'modulo': 'perfis e oportunidades'})
existing = {r['ID']: r for r in rows}
for n, data in definition.items():
identifier = f'AUD-PROFILE-{n:02d}'
references = assigned[n]
# Some requirements are direct reconciliation decisions, without a dedicated paragraph.
if not references:
references = [{'id': identifier + '-USER-20261010', 'pedido': data['title'], 'fontes': ['Pedido direto do responsável10/10/2026 nesta conversa'],
'aceitacao': data['acceptance'], 'estado': data['state'], 'variantes': 'Reconciliação funcional com v1 e auditoria existente.', 'modulo': 'perfis e oportunidades'}]
row = {field: '' for field in fields}
row.update({'ID': identifier, 'Pedido/trecho': data['title'], 'Fonte/ocorrências': '\n'.join(ref['fontes'][0] for ref in references),
'Módulo': 'perfis e oportunidades', 'Rotas': '/dashboard; /perfil/promotor; /minhas-oportunidades; /perfis/gestao; /perfis/auditoria; /perfis/*',
'Implementação encontrada': data['implementation'], 'Disponibilidade ativa': 'Leitura10/10: instalação040000ausente; PORTAL_PROFILE_ACCESS_ENABLED=false; nenhuma conta ativa alterada. QA18097: schema instalado e flagtrue.',
'Evidência funcional': data['evidence'] + ' | Evidências privadas sanitizadas: profiles-v1-http-qa.json; profiles-v1-recovery-audit-qa.json; profiles-v1-final-qa.xml; profiles-v1-current-version-qa.json quando disponível.',
'Estado': data['state'], 'Lacuna': data['gap'], 'Dependência': 'Reutilizar entidades existentes; QA isolada; aceitação visual pendente. Dependências concretas descritas no critério/lacuna, sem bloquear trabalho independente.',
'Critério de aceitação': data['acceptance'], 'Prioridade': 'P0' if n in {16,17,24} else 'P1',
'Versões divergentes': 'Exemplar anterior e v1 preservados. Administração integral anterior passa a controlada na v1; edição organizacional aprovada nesta conversa; Promotor solicita arquivo; gestores legados não se tornam Superutilizadores. p337/p355/SU04/SU05/GOV01 acrescentam governança/atomicidade.',
'IDs das linhas de origem': ';'.join(ref['id'] for ref in references), 'Quantidade de linhas de origem': str(len(references)),
'Critérios e versões por origem (JSON)': json.dumps(references, ensure_ascii=False, separators=(',', ':')),
'Interface': 'Preparada em QA, sem demonstração visual. C37 e A/B mantidos pendentes.' if n != 27 else 'Não aplicável: decisão documental restrita, sem substituição de autenticação.',
'API': data['evidence'], 'Gravação': 'QA: entities canónicas/papéis/pedidos/workflow/audit_events; gravações sintéticas somente. Não inferir persistência dos fluxos descritos como pendentes.',
'Recuperação': 'Pedidos e rascunho15 recuperados em novas sessõesMFA; auditoria/revogação/dupla comprovadas nos escopos descritos. Outros critérios pendentes não fechados.',
'Resultado apresentado': 'Props/JSON/mensagens de autorização verificados; resultado renderizado no navegador pendente.' if n != 27 else 'Fortify mantido; não há integraçãoKeycloak instalada/declarada.',
'Consolidação': 'Mesmo CSV canónico.32requisitos funcionais novos; parágrafos de fontes preservados como ocorrências, incluindo estrutura/tabelas/casos, sem os contar como requisitos independentes.'})
if identifier in existing:
existing[identifier].update(row)
else:
rows.append(row)
marker = '[Perfis v1 QA10/10/2026]'
for identifier in ['AUD-OP-01', 'AUD-OP-02', 'AUD-OP-03', 'AUD-OP-04', 'AUD-DASH-01', 'AUD-UI-01', 'AUD-OPS-01']:
row = existing.get(identifier)
if not row: continue
for field, text in {'Implementação encontrada': 'Reutilização/permissões organizacionais e workflow parcial nos AUD-PROFILE-01–32; nenhum sistema paralelo de identidades.',
'Evidência funcional': 'Dashboard→novo draft15→recuperaçãoMFA→coedição21/22 HTTP; tipos/org/multirrole403; requestArchive persiste. Submissão422 por contrato completo ausente. VisualC37/C43/A-B pendente.',
'Lacuna': 'Critérios específicos preservados; não confundir workflow parcial/props de menu com submissão/aprovação/publicação completas ou demonstração visual.'}.items():
if marker not in row[field]: row[field] += ' | ' + marker + ' ' + text
if identifier == 'AUD-OP-03': row['Estado'] = 'parcial'
if identifier == 'AUD-OP-03':
for field, text in {
'Interface': 'Navegação/pedido de arquivo preparados; revisão visual e formulário especializado completos pendentes.',
'API': 'Workflow parcial v1 com autorização/razão/etag; submit real422 por contrato incompleto. Revisão/correção positiva apenas em fixture explicitamente submetida; não é submissão integral.',
'Gravação': 'QA: owner/org/contributors, pedido de arquivo, versões/transições parciais e audit_events preservados; zero decisões/publicações ativas.',
'Recuperação': 'Novo rascunho15 e contexto recuperados em nova sessãoMFA; fixture verifica revisão/correção. Ciclo integral publicado ainda ausente.',
'Resultado apresentado': 'Estado corrente parcial; props/mensagens API comprovadas, interface e aprovação/publicação reais pendentes. Registos de não implementação anteriores são históricos.'
}.items():
if marker not in row[field]: row[field] += ' | ' + marker + ' ' + text
for identifier in ['AUD-GEO-02', 'AUD-GEO-03']:
row = existing[identifier]
field = 'Evidência funcional'
if marker not in row[field]:
row[field] += ' | ' + marker + ' ConsumidorPromotor autorizado:11províncias, CaboDelgado selecionada/guardada/recuperada noutra sessão e1área geométricaAPI. Novo endpoint reutiliza catálogo/serviço; não demonstra mapa renderizado nem resolve31distritos/157postos pendentes. QA393privadas,ativo0; publicação separada.'
# Before-source entries must be byte-equivalent in meaning; state/evidence reconciliation cannot rewrite them.
for old in before:
current = next(r for r in rows if r['ID'] == old['ID'])
for key in ['IDs das linhas de origem', 'Quantidade de linhas de origem', 'Critérios e versões por origem (JSON)', 'Pedido/trecho', 'Critério de aceitação', 'Versões divergentes']:
assert current[key] == old[key], (old['ID'], key)
assert len(rows) == len({r['ID'] for r in rows}) == 3386
all_references = [item for row in rows for item in json.loads(row['Critérios e versões por origem (JSON)'])]
profile_refs = [item for item in all_references if item['id'].startswith(('PROFILEV1-', 'PROFILEOLD-'))]
assert len(profile_refs) == len({r['id'] for r in profile_refs}) == 1001
with MATRIX.open('w', encoding='utf-8-sig', newline='') as handle:
writer = csv.DictWriter(handle, fieldnames=fields); writer.writeheader(); writer.writerows(rows)
states = collections.defaultdict(lambda: {'requisitos': 0, 'linhas_origem': 0})
for row in rows:
states[row['Estado']]['requisitos'] += 1
states[row['Estado']]['linhas_origem'] += int(row['Quantidade de linhas de origem'])
source_hash = hashlib.sha256(json.dumps(all_references, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
counts = {'requisitos': len(rows), 'linhas_matriz': len(rows), 'linhas_origem': len(all_references), 'por_estado': dict(states),
'conservation_sha256': source_hash, 'before_source_occurrences_preserved': 5830, 'new_document_paragraph_references': 1001,
'new_direct_user_references': len(all_references)-5830-1001,
'metodo': 'Mesma matriz;3354pedidos existentes e suas5830referências preservados,32requisitos verificáveis de perfis. Parágrafos estruturais são referências, não pedidos independentes. AUD-OP-03 passa a parcial pelo incremento, sem fechar workflow.',
'browser_current_version_verified': False}
(PRIVATE / 'contagens-requisitos.json').write_text(json.dumps(counts, ensure_ascii=False, indent=2), encoding='utf-8')
proof = {'captured_at': datetime.now(timezone.utc).isoformat(), 'all_previous_ids_preserved': True, 'previous_source_references_preserved': True,
'sources': [{k:v for k,v in source.items() if k != 'paragraphs'} for source in sources], 'accessible_corpus_files': len(paths),
'inventory_entries': len(inventory), 'inventory_entries_added_this_pass': len(new_inventory), 'inventory_added_relative_prior586': len(inventory)-586,
'office_lock_markers_inventoried_by_bytes': sum(Path(p).name.startswith('~$') for p in paths), 'inventory_unreadable': errors,
'matrix': counts, 'qa_only_implementation': True, 'active_writes': 0}
(PRIVATE / 'profiles-v1-reconciliation-proof.json').write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({k:v for k,v in proof.items() if k not in {'sources','matrix'}},ensure_ascii=False))
print(json.dumps(counts,ensure_ascii=False))
# Preserve later workflow evidence when rebuilding the original profile reconciliation.
if (PRIVATE / 'opportunity-workflow-http-qa.json').exists():
import runpy
runpy.run_path(str(ROOT / 'tools/portal-audit-workflow-update.py'), run_name='__main__')