376 lines
37 KiB
Python
376 lines
37 KiB
Python
"""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__')
|