300 lines
39 KiB
Python
300 lines
39 KiB
Python
"""Prepare targeted Artifact Tool patches for the existing audit; Python does not author CSV files."""
|
||
import collections
|
||
import csv
|
||
import hashlib
|
||
import json
|
||
import sys
|
||
import shutil
|
||
import subprocess
|
||
from datetime import datetime, timezone, timedelta
|
||
from pathlib import Path
|
||
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
csv.field_size_limit(16 * 1024 * 1024)
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
DOC = ROOT / 'docs/investmoz/v2/auditoria-portal'
|
||
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
|
||
OUT = PRIVATE / 'ab-countries-finalize-20261010'
|
||
IDS_AB = {'AUD-GUIDE-01', 'AUD-SIM-01', 'AUD-SIM-02', 'AUD-DESK-01', 'AUD-DESK-02', 'AUD-CRM-01', 'AUD-UI-01'}
|
||
IDS_COUNTRY = {f'AUD-COUNTRY-{n:02d}' for n in [1, 2, 3, 4, 6, 7]} | {'AUD-STATS-01'}
|
||
IDS = IDS_AB | IDS_COUNTRY
|
||
rows = list(csv.DictReader((DOC / 'matriz-requisitos.csv').open(encoding='utf-8-sig', newline='')))
|
||
if '--inspect' in sys.argv:
|
||
print(json.dumps([{k: row[k][:350] for k in ['ID', 'Pedido/trecho', 'Critério de aceitação']} for row in rows if row['ID'] in IDS], ensure_ascii=False, indent=2))
|
||
sys.exit(0)
|
||
if '--inspect-country-status' in sys.argv:
|
||
print(json.dumps([{k: row[k][-1700:] for k in ['ID', 'Estado', 'Disponibilidade ativa', 'Evidência funcional', 'Lacuna']} for row in rows if row['ID'] in {'AUD-COUNTRY-01', 'AUD-COUNTRY-04', 'AUD-STATS-01'}], ensure_ascii=False, indent=2))
|
||
sys.exit(0)
|
||
if '--verify' in sys.argv:
|
||
first = json.loads((OUT / 'csv-edits-initial.json').read_text(encoding='utf-8'))
|
||
last = json.loads((OUT / 'csv-edits.json').read_text(encoding='utf-8'))
|
||
checks = []
|
||
for baseline in first['files']:
|
||
filename = baseline['filename']
|
||
now = list(csv.reader((DOC / filename).open(encoding='utf-8-sig', newline='')))
|
||
before = baseline['rows']
|
||
assert len(now) == len(before) == 3394 and now[0] == before[0]
|
||
headers = now[0]
|
||
idcol = headers.index('ID')
|
||
permissions = {}
|
||
for packet in [first, last]:
|
||
edit = next(e for e in packet['files'] if e['filename'] == filename)
|
||
for patch in edit['patches']:
|
||
permissions.setdefault(patch['row'], set()).update(patch['values'])
|
||
# SIM01 was included between the initial thirteen-row packet and the later search-only refinement.
|
||
# Its approved columns are the same narrow evidence/gap columns declared in the initial packet; identity/state/source columns remain forbidden.
|
||
sim_index = next(i for i, r in enumerate(before) if i and r[idcol] == 'AUD-SIM-01')
|
||
permissions.setdefault(sim_index, set()).update(baseline['allowed_columns'])
|
||
changed_ids = set()
|
||
for i, (old, current) in enumerate(zip(before[1:], now[1:]), 1):
|
||
assert old[idcol] == current[idcol]
|
||
for j, (old_value, value) in enumerate(zip(old, current)):
|
||
if old_value != value:
|
||
assert old[idcol] in IDS and headers[j] in permissions[i], (filename, old[idcol], headers[j])
|
||
changed_ids.add(old[idcol])
|
||
if filename == 'matriz-requisitos.csv' and old[idcol] in IDS:
|
||
field = headers.index('Critérios e versões por origem (JSON)')
|
||
old_versions, current_versions = json.loads(old[field]), json.loads(current[field])
|
||
assert len(old_versions) == len(current_versions)
|
||
for original, updated in zip(old_versions, current_versions):
|
||
for key, value in original.items():
|
||
assert updated[key] == value
|
||
assert changed_ids == IDS
|
||
checks.append({'filename': filename, 'changed_requirements': len(changed_ids), 'preserved_ids_order_and_other_cells': True,
|
||
'sha256': hashlib.sha256((DOC / filename).read_bytes()).hexdigest()})
|
||
scope = json.loads((OUT / 'scope-proof.json').read_text(encoding='utf-8'))
|
||
assert all(hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == digest for name, digest in scope['source_hashes'].items())
|
||
assert hashlib.sha256((ROOT / 'public/build/manifest.json').read_bytes()).hexdigest() == scope['active_manifest_sha256']
|
||
states = dict(collections.Counter(r['Estado'] for r in rows))
|
||
assert states == {'parcial': 3235, 'não implementado': 145, 'dependente de decisão concreta': 5, 'implementado mas não ativado': 1, 'concluído e demonstrado': 7}
|
||
occurrences = sum(int(r['Quantidade de linhas de origem']) for r in rows)
|
||
assert occurrences == 6845
|
||
proof = {'result': 'passed', 'canonical_requirements': len(rows), 'rows': len(rows), 'source_occurrences': occurrences,
|
||
'states': states, 'files': checks, 'versions_and_references_preserved': True, 'tested_source_hashes_still_current': True,
|
||
'active_manifest_unchanged': True, 'active_writes': 0}
|
||
(OUT / 'preservation-verification.json').write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(json.dumps(proof, ensure_ascii=False))
|
||
sys.exit(0)
|
||
def load_receipt(name, encoding='utf-8-sig'):
|
||
return json.loads((PRIVATE / name).read_text(encoding=encoding))
|
||
|
||
ab = load_receipt('investor-ab-remaining-chromium-20261010.json')
|
||
country = load_receipt('country-paths-chromium-20261010.json')
|
||
source = load_receipt('form-entry-source-chromium-20261010.json')
|
||
search = load_receipt('country-search-chromium-20261010.json')
|
||
assert search['result'] == 'passed_read_only_four_country_searches' and len(search['search']) == 4 and search['business_data_writes'] == 0
|
||
assert all(e['method'] in ['GET', 'HEAD'] for e in search['events'])
|
||
sql_ab = load_receipt('investor-ab-remaining-persistence-qa.json')
|
||
sql_country = load_receipt('country-paths-persistence-qa.json')
|
||
sql_source = load_receipt('form-entry-source-persistence-qa.json')
|
||
status_bytes = (PRIVATE / 'ab-countries-final-status-qa.json').read_bytes()
|
||
status = json.loads(status_bytes.decode('utf-16' if status_bytes.startswith(b'\xff\xfe') else 'utf-8-sig'))
|
||
for receipt in [ab, country, source]:
|
||
assert receipt['result'] == 'passed' and receipt['target'] == 'desk_restore_20261009_174726_test:55441'
|
||
assert receipt['active_writes'] == 0
|
||
assert sql_ab['read_only'] and sql_country['read_only'] and sql_source['read_only']
|
||
assert status['target'] == {'database': 'desk_restore_20261009_174726_test', 'port': 55441}
|
||
assert country['baseline_known_origin'] == 0 and country['known_origin_after'] == 4
|
||
assert len(ab['accessibility']) == 6 and all(not r['violations'] and r['scrollWidth'] <= r['viewport'] for r in ab['accessibility'])
|
||
stamp = datetime.now(timezone.utc)
|
||
OUT.mkdir(exist_ok=True)
|
||
tag = '[QA A/B e países 10/10/2026] '
|
||
search_note = ('country-search-chromium-20261010.json: quatro formulários públicos, pesquisa Portugal→PT, França→FR conservando seleçãoPT '
|
||
'e pesquisa independente África do Sul→ZA; Chromium390px, somente leitura, sem autenticação ou novos envios.')
|
||
gaps = {
|
||
'AUD-GUIDE-01': 'Cenários internos A/B concluídos em QA, incluindo preparação paralela. Restam regras/evidências e variações condicionais das demais etapas S01–S19; âmbito/período e decisões externas documentadas por percurso real. Não repetir A/B ou C37 para esses critérios.',
|
||
'AUD-SIM-02': 'Equilíbrio e necessidade de financiamento A/B associados e recuperados, sem mistura. Restam reconciliação de totais/fluxo/cenários mais complexos, fiscalidade com parâmetros aplicáveis e períodos/versões distintos, PDFs/derivadas e demais consumidores. Resultado histórico A sem moeda mantém-se visível como anterior, sem atribuir-lhe MZN por inferência.',
|
||
'AUD-SIM-01': 'Rascunhos contextuais A/B guardados/recuperados noutra sessão e resultados no dossier correto já demonstrados; não repetir esse percurso. Restam variantes não contextuais/anónimas, expiração/purga e operações de outros consumidores/perfis, conforme versões existentes. Preservar o acesso direto e isolamento de projetos.',
|
||
'AUD-DESK-01': 'A/B demonstram preparação DRAFT e recuperação. Restam submissão quando aplicável, 422 por campo, protocolo/recibo, mensagens, transições e operações documentais por tipo/perfil. DRAFT não equivale a submissão ou decisão externa.',
|
||
'AUD-DESK-02': 'Reutilização contextual A/B concluída em QA; C88 preenchimento explícito da descrição vazia corrigido com fonte/revisão. Restam entidades empresariais, submissões e pedidos legados/representação organizacional por consumidor, conferência documental e demais critérios S01–S19; sem copiar dossier ou conta.',
|
||
'AUD-CRM-01': 'Tarefa S02 B concluída enquanto S10 permanecia à espera; gestor distinto e dados financeiros privados preservados. Restam conversão de manifestações sem duplicar investidor, responsável autorizado/próxima tarefa antes do fecho e filas/atribuições por perfil. Não depende de CRM externo.',
|
||
'AUD-UI-01': 'A/B internos concluídos por composição das provas anteriores com os critérios restantes; seis varreduras móveis 320/390px sem overflow/violações automáticas. Restam rotas e ações de outros consumidores/perfis, dispositivos reais, leitor de ecrã, contraste/foco manual e editor especializado. C37 QA18 continua encerrado.',
|
||
'AUD-COUNTRY-01': 'Residência PT QA18 guardada na prova anterior e recuperada noutra sessão neste lote. Não repetir esse critério. Restam cadastro inicial/outros perfis e variações de validação não demonstradas visualmente; disponibilidade ativa não confirmada por este ensaio QA.',
|
||
'AUD-COUNTRY-02': 'Sem nova prova da constituição da entidade neste lote: país do representante no formulário público Empresa não prova país mestre de constituição. Demonstrar constituição distinta da residência, gravação/versão/recuperação e recusa de outra conta no consumidor empresarial existente.',
|
||
'AUD-COUNTRY-03': 'PT/ZA públicos e Balcão A/B recuperados; país, origem declarada e página de entrada apresentados separadamente. Restam variações condicionais/erros e serviços do Balcão ainda não percorridos, inclusive estatística positiva de pedidos submetidos; não inferir origem de residência, IP, capital ou constituição.',
|
||
'AUD-COUNTRY-04': 'Quatro formulários públicos agora submetidos e recuperados com PT/ZA, filtros e estatística positiva. Restam interesses associados a oportunidade/negócio, apoios/parcerias condicionais, recuperação pela jornada/carteira e feedback visual de erros de país/legados conforme consumidor. API/replay/compatibilidade históricos preservados, sem os contar como visual novo.',
|
||
'AUD-COUNTRY-06': 'Wizard de negócios PT/FR recuperado numa nova sessão, sem regravar. Restam submissão/interesse no negócio público, efeitos nas estatísticas do conjunto Negócios, variações condicionais e feedback de validação/legados por perfil; negar outras contas conforme política existente.',
|
||
'AUD-COUNTRY-07': 'Rascunho fundiário PT/FR recuperado numa nova sessão, sem regravar. Restam submissão/conversão do apoio, estatísticas específicas e seletores/documentos territoriais aplicáveis por perfil. Não criar nacionalidade, origem ou relações territoriais por inferência.',
|
||
'AUD-STATS-01': 'Interface mostra origem conhecida ZA=4 após os quatro envios, antes do ensaio adicional C90; conjuntos mantidos separados. Restam positivos de Balcão/Negócios submetidos, combinações/filtros da jornada e CRM e demais perfis. Rascunhos excluídos; não informado permanece; não somar como investidores únicos. Um quinto registo sintético C90 posterior é separado desta captura.',
|
||
}
|
||
ab_evidence = ('A/B internos concluídos em QA: provas anteriores de criação/gravação/nova sessão/permissões compostas com '
|
||
'investor-ab-remaining-chromium-20261010.json e investor-ab-remaining-persistence-qa.json. '
|
||
'A terra/financiamento=no, S10 dispensada, equilíbrio20, necessidade0MZN; B=yes, S10waiting_external, equilíbrio20, necessidade70000MZN. '
|
||
'Resultado/draft/dossier corretos, BalcãoDRAFT com título do dossier/resumo da ferramenta/PT/ZA, tarefaS02done posterior à esperaS10 '
|
||
'e preparação independente habilitada; cinco fases/19etapas preservadas. Seis verificações390/320px sem overflow ou violações axe. '
|
||
'C88 ação explícita recuperou descrição vazia de A, guardando revisão antiga/fonte(v3)/auditoria. Sem recriar dossiers/resultados/tarefas ou repetir C37. Ativo não alterado.')
|
||
country_evidence = ('country-paths-chromium-20261010.json + country-paths-persistence-qa.json: '
|
||
'formulários/investidor,empresa,apoio e manifestar-interesse:249países, PT/ZA, UI→POST→protocolo→gestão autorizada→filtros independentes→origemZA=4. '
|
||
'Uma linha/consentimento/recibo de idempotência por envio; SQLQA READONLY. ResidênciaPT, negócio/fundiárioPT/FR recuperados emnova sessão sem regravar. '
|
||
'C89 nomes acessíveis dos seletores corrigidos; quatro exames390px sem overflow/violações automáticas. '
|
||
'C90 source_page dos novos envios mantém página de entrada mesma origem; formulário/investidor demonstrado por UI/API/SQL e página da gestão, '
|
||
'form-entry-source-*qa.json; registos antigos não reescritos. Rejeições DNS dos emails sintéticos foram validação esperada; notificaçõesQA desativadas. Ativo não alterado.')
|
||
files = []
|
||
for filename in ['matriz-requisitos.csv', 'backlog-priorizado.csv']:
|
||
original = DOC / filename
|
||
data = list(csv.reader(original.open(encoding='utf-8-sig', newline='')))
|
||
headers, patches = data[0], []
|
||
for index, row in enumerate(data[1:], 1):
|
||
old = dict(zip(headers, row))
|
||
if old['ID'] not in IDS:
|
||
continue
|
||
requirement = old['ID']
|
||
if filename.startswith('matriz'):
|
||
previous_variants = json.loads(old['Critérios e versões por origem (JSON)'])
|
||
if previous_variants and all(v.get('incremento_ab_paises_20261010', {}).get('criterios_restantes') == gaps[requirement] for v in previous_variants):
|
||
values = {}
|
||
if requirement in {'AUD-COUNTRY-04', 'AUD-UI-01'} and 'country-search-chromium-20261010.json' not in old['Evidência funcional']:
|
||
values = {'Evidência funcional': old['Evidência funcional'] + ' | ' + tag + search_note,
|
||
'Interface': old['Interface'] + ' | ' + tag + search_note}
|
||
for variant in previous_variants:
|
||
variant['incremento_pesquisa_paises_20261010'] = {'registado_em': stamp.isoformat(), 'evidencia': search_note, 'ativacao': False}
|
||
values['Critérios e versões por origem (JSON)'] = json.dumps(previous_variants, ensure_ascii=False, separators=(',', ':'))
|
||
patches.append({'row': index, 'values': values})
|
||
continue
|
||
elif old['Pendência'].startswith(tag + gaps[requirement] + ' | '):
|
||
values = {'Aceitação': old['Aceitação'] + ' | ' + tag + search_note} if requirement in {'AUD-COUNTRY-04', 'AUD-UI-01'} and 'country-search-chromium-20261010.json' not in old['Aceitação'] else {}
|
||
patches.append({'row': index, 'values': values})
|
||
continue
|
||
evidence = ab_evidence if requirement in IDS_AB else country_evidence
|
||
if filename.startswith('matriz'):
|
||
values = {'Lacuna': tag + gaps[requirement] + ' | Histórico preservado/superado apenas nos critérios identificados: ' + old['Lacuna']}
|
||
if requirement != 'AUD-COUNTRY-02':
|
||
increment = {'Evidência funcional': evidence,
|
||
'Interface': 'Chromium, contas QA autorizadas; critérios restantes A/B e apresentação dos dados por formulário demonstrados; screenshots/DOM no recibo específico.',
|
||
'API': 'Respostas UI e GET de recuperação com destino QA verificado; evidências vinculadas a IDs do mesmo dossier/formulário. Regresso ao Balcão DRAFT, sem decisão externa.',
|
||
'Gravação': 'Tabelas/entidades existentes; revisão e proveniência de C88; cinco intakes sintéticos (quatro países + um C90), sem duplicar dossiers ou contas; nenhuma escrita ativa.',
|
||
'Recuperação': 'Dossiers, resultados e pedidos A/B existentes; perfil/negócio/fundiário noutra sessão; protocolos públicos recuperados pela gestão autorizada.',
|
||
'Resultado apresentado': 'A=0/B=70000MZN e equilíbrio20 no dossier correto; preparaçãoBalcão, pendênciaS10 e tarefaS02done; PT/ZA, filtros independentes e estatística positiva; página de entradaC90 distinta.',
|
||
'Consolidação': 'Critérios internos A/B concluídos emQA; restantes critérios explicitados por requisito. Estados globais e todas as referências/versões de origem preservados.'}
|
||
# Do not claim country evidence for financial computations or A/B evidence for master-company country.
|
||
if requirement not in IDS_AB:
|
||
increment['API'] = 'Públicos POST302/201, protocolo, recuperação na gestão e filtros PT/ZA; estatística positiva. Business/land/profile GET nova sessão. C90 referente externo/não permitido é ignorado.'
|
||
increment['Gravação'] = 'Quatro intakes PT/ZA e um adicional C90, um registo por formulário/ensaio; perfil/negócios/fundiário somente leitura neste lote. SQL QA confirma país/origem/consentimento/recibo. Sem migração ou escrita ativa.'
|
||
increment['Resultado apresentado'] = 'PT/ZA em quatro formulários/gestão, origem conhecidaZA=4 na captura; residênciaPT e negócios/fundiárioPT/FR na nova sessão. C90 página de entrada separada. Não é prova do país mestre de constituição.'
|
||
values.update({field: old[field] + ' | ' + tag + value for field, value in increment.items()})
|
||
variants = json.loads(old['Critérios e versões por origem (JSON)'])
|
||
for variant in variants:
|
||
variant['incremento_ab_paises_20261010'] = {'registado_em': stamp.isoformat(), 'ambito': 'QA somente',
|
||
'criterios_ab_internos': 'concluído e demonstrado em QA' if requirement in IDS_AB else 'não se aplica a esta linha',
|
||
'evidencia': evidence if requirement != 'AUD-COUNTRY-02' else 'Nenhuma nova prova do país mestre de constituição; não extrapolar o formulário público Empresa.',
|
||
'criterios_restantes': gaps[requirement], 'ativacao': False}
|
||
values['Critérios e versões por origem (JSON)'] = json.dumps(variants, ensure_ascii=False, separators=(',', ':'))
|
||
else:
|
||
values = {'Pendência': tag + gaps[requirement] + ' | Histórico preservado: ' + old['Pendência'],
|
||
'Aceitação': old['Aceitação'] + ' | ' + tag + ('Constituição empresarial continua pendente; não foi demonstrada pelo formulário público.' if requirement == 'AUD-COUNTRY-02' else evidence),
|
||
'Correção/componentes': old['Correção/componentes'] + ' | ' + tag + 'Reutilizar InvestorPath/VirtualDesk e os formulários/catálogos existentes. C88 preenchimento explícito só de descrição vazia com versão/fonte; C89 rótulos; C90 source_page e apresentação. Não repetir critérios concluídos.'}
|
||
patches.append({'row': index, 'values': values})
|
||
assert len(data) == 3394 and len(patches) == len(IDS)
|
||
files.append({'filename': filename, 'input_sha256': hashlib.sha256(original.read_bytes()).hexdigest(), 'rows': data,
|
||
'patches': patches, 'allowed_columns': sorted({key for patch in patches for key in patch['values']}), 'append': []})
|
||
packet_path = OUT / 'csv-edits.json'
|
||
if packet_path.exists() and not (OUT / 'csv-edits-initial.json').exists():
|
||
shutil.copyfile(packet_path, OUT / 'csv-edits-initial.json')
|
||
elif packet_path.exists():
|
||
shutil.copyfile(packet_path, OUT / ('csv-edits-previous-' + stamp.strftime('%Y%m%d_%H%M%S_%f') + '.json'))
|
||
packet_path.write_text(json.dumps({'files': files}, ensure_ascii=False), encoding='utf-8')
|
||
|
||
def section(filename, marker, text):
|
||
file = DOC / filename
|
||
prior = file.read_text(encoding='utf-8-sig')
|
||
begin, end = f'<!-- {marker}:begin -->', f'<!-- {marker}:end -->'
|
||
if begin in prior:
|
||
start_index, end_index = prior.index(begin), prior.index(end) + len(end)
|
||
prior = prior[:start_index] + prior[end_index:].lstrip('\n')
|
||
file.write_text(begin + '\n' + text.strip() + '\n\n' + end + '\n\n' + prior, encoding='utf-8')
|
||
|
||
summary = f'''## A/B internos demonstrados em QA e lote de países — {stamp.astimezone(timezone(timedelta(hours=2))).isoformat()}
|
||
|
||
**Cenários A/B internos concluídos e demonstrados por composição de provas, sem repetir os ensaios já aceites. C37 permanece encerrado apenas na autenticação QA18.** QA `http://127.0.0.1:18097`, base `desk_restore_20261009_174726_test:55441`. Nenhuma escrita ativa; não houve nova instalação ou migração. Mercado Financeiro continua privado.
|
||
|
||
| Critério restante | Cenário A | Cenário B | Prova |
|
||
| --- | --- | --- | --- |
|
||
| Aplicabilidade | Terra e financiamento externo Não; S10 dispensada só da preparação | Terra e financiamento Sim; S10 aguarda informação externa | Interface e GET do mesmo dossier; cinco fases/19 etapas |
|
||
| Resultado no dossier correto | Equilíbrio:20 unidades; necessidade:0 MZN; rascunho/resultado próprios | Equilíbrio:20 unidades; necessidade:70.000 MZN; rascunho/resultado próprios | Resultado visível com UUID; API e SQL ligam dossier/rascunho/evidência, sem mistura |
|
||
| Preparação no Balcão | Pedido existente; C88 reutilizou explicitamente resumo da ferramenta, v3, conservando revisão vazia anterior | Pedido existente recupera título do dossier e resumo da ferramenta | Ambos DRAFT, PT/ZA, sem protocolo; origem da reutilização auditada |
|
||
| Avanço independente | Acompanhamento interno existente recuperado | S02 em preparação e tarefa concluída após S10 entrar em espera; S10 continua à espera | Cronologia SQL/eventos e controles visíveis habilitados; não resetar/recriar tarefa |
|
||
|
||
Interface: seis verificações dirigidas A/B em 390/320 px, sem overflow ou violações automáticas axe; imagens de resultados/etapas inspecionadas. Isso não encerra dispositivos reais, leitor de ecrã, contraste/foco manual ou acessibilidade integral. Resultados anteriores de A sem moeda foram preservados; o resultado identificado deste cenário é MZN.
|
||
|
||
Provas anteriores reutilizadas: `investor-ab-chromium-20261010.json` (criar→guardar→nova sessão→recuperar→ferramenta→resultado→Balcão→gestor→recuperar/permissões), `c37-manual-close-20261010/acceptance.json`. Provas restantes: `investor-ab-remaining-chromium-20261010.json` e `investor-ab-remaining-persistence-qa.json`. Balcão em preparação não afirma submissão, aprovação de terra, financiamento ou decisão externa. O fecho A/B não encerra integralmente GUIDE01, SIM02, DESK01/02, CRM01 ou UI01.
|
||
|
||
| Formulário/consumidor | Evidência nova | Critério restante |
|
||
| --- | --- | --- |
|
||
| Público Investidor `/formularios/investidor` | PT/ZA→POST→protocolo→gestão→filtros; um registo/consentimento/recibo | Variações condicionais, jornada e erros/legados por consumidor |
|
||
| Público Empresa `/formularios/empresa` | Mesmo percurso; dados da empresa/representante recuperados | Não demonstra país mestre de constituição da entidade (COUNTRY02) |
|
||
| Apoio `/apoio` | PT/ZA→protocolo→recuperação; uma gravação | Apoios condicionais, documentos/encaminhamentos por tipo |
|
||
| Manifestação `/manifestar-interesse` | PT/ZA→POST201→protocolo→recuperação/filtros | Oportunidades/negócios associados e recuperação na carteira |
|
||
| Perfil QA18 | ResidênciaPT recuperada noutra sessão | Cadastro inicial/outros perfis e condições visuais específicas |
|
||
| Negócio/fundiário existentes | PT/FR recuperados noutra sessão, sem regravar | Submissão, estatística do conjunto próprio, condições/territórios/documentos |
|
||
| Estatísticas de gestão | OrigemZA passou0→4 nas quatro manifestações; filtros país/origem independentes | Positivos de Balcão/Negócios submetidos, jornada/CRM e demais perfis |
|
||
| Página de entrada (C90) | Um quinto envio sintético após a captura acima preserva `/formularios/investidor`, apresentado separadamente de PT/ZA | Registos históricos não são reatribuídos por inferência; ampliar condições do consumidor |
|
||
|
||
Países/protocolos/SQL: `country-paths-chromium-20261010.json`, `country-paths-persistence-qa.json`, `form-entry-source-chromium-20261010.json`, `form-entry-source-persistence-qa.json`. C89 corrigiu nomes acessíveis indistintos (“País”, “País”) nos quatro formulários; quatro exames em 390 px sem overflow/violações automáticas. Pesquisa por formulário também demonstrada em `country-search-chromium-20261010.json`: Portugal→PT, França→FR sem perder a seleção e pesquisa independente África do Sul→ZA; somente leitura, sem repetir envios. Rejeição DNS de emails sintéticos foi comportamento esperado, sem pedido persistido, não falha do portal. Notificações de saída QA desativadas; nenhum worker externo executado.
|
||
|
||
**C88/C89/C90 corrigidos em QA.** C90 eliminou `source_page='/apoio'` fixo para novos envios de formulários de entrada conhecidos da mesma origem; referentes externos, administrativos ou de outra porta não são aceites. Dados anteriores preservados. A página de entrada registada aparece na gestão, distinta do país e da origem declarada.
|
||
|
||
Mesma matriz: **3393 requisitos/3393 linhas/6845 ocorrências**. Estados globais preservados:3235 parciais,145 não implementados,5 dependentes de decisão,1 implementado mas não ativado,7 concluídos e demonstrados. Catorze linhas receberam critérios/evidências específicos; COUNTRY02 recebeu apenas delimitação da lacuna, sem extrapolar prova. O estado global parcial não reabre os subcritérios A/B agora concluídos; os critérios restantes estão individualizados no backlog.
|
||
|
||
Próximo trabalho executável: mapas/seletores de projetos, clusters e corredores (guardar/recuperar relações e descendentes); documentos/PDFs/exportações/derivadas por consumidor e perfil; workflow QA21→QA24→QA25 com alterações materiais/rejeição/suspensão. País de constituição, Balcão/Negócios submetidos e condições territoriais próprias continuam independentes.173 correspondências anteriores e Mandie/Guro ambíguo preservados. Perfis modulares/SLA e regra de restauração conservam decisões próprias; não iniciar novas jornadas educativas.
|
||
|
||
Código/build preparados e QA demonstrada; **funcionalidade ativa não validada/ativada por este lote**. Manifesto ativo `{status['active_manifest_sha256']}` intacto. Instalação futura continua030000→040000→09/190000, só recursos ausentes; correções, importação cartográfica e publicação separados. O backup anterior não comprova a revisão global nova. Fixar candidata e renovar/ligar provas de backup/restauro, compatibilidade e reversão no clone do ativo antes de qualquer corte autorizado.
|
||
'''
|
||
section('relatorio-executivo.md', 'qa-ab-countries-20261010', summary)
|
||
remaining_table = '\n'.join(f'| {requirement} | {gap} |' for requirement, gap in gaps.items())
|
||
section('backlog.md', 'qa-ab-countries-remaining-20261010', f'''## Critérios restantes após o fecho dos cenários A/B internos — {stamp.astimezone(timezone(timedelta(hours=2))).isoformat()}
|
||
|
||
A/B internos concluídos em QA por provas anteriores mais demonstração dos critérios restantes. Não repetir criação, gravação, recuperação, logout/C37 ou resultado já demonstrados. C88/C89/C90 corrigidos; sem ativação. A fotografia atual dos critérios é esta; pendências históricas abaixo ficam superadas somente no âmbito demonstrado.
|
||
|
||
| Requisito existente | Critérios restantes / próxima ação |
|
||
| --- | --- |
|
||
{remaining_table}
|
||
| AUD-GEO-01/02/03 | Guardar e recuperar seletores/relações e limites em projetos/clusters/corredores; descendentes e mapas consumidores restantes. Geometria disponível não prova apresentação;173 correspondências preservadas, Mandie/Guro ambíguo; tiles externos e localidades sem geometria têm pendências próprias. Não repetir mapa/localizações da oportunidade51 já demonstrado. |
|
||
| AUD-DOC-01 e operações documentais | PDFs gerados, exportações, partilhas e derivadas por consumidor/tipo/perfil, hash/scan da versão e preservação do original. ClamD real anterior permanece provado por original; só operações ainda não verificadas ficam pendentes. |
|
||
| AUD-OP-03/04/05; perfis/workflow | Demonstrar pela interface Promotor21→submissão→Validador24→correção→aprovação→Publicador25; rejeição fundamentada, alterações materiais/suspensão, múltiplos papéis e recusa de autoaprovação. Restauração exige ator/regra/estado de retorno explícitos; não conceder por inferência. Contratos especializados/Response§27 mantêm lacunas próprias. |
|
||
| AUD-OPS-01 | Fixar candidata global; renovação de provas correspondentes, backup/restauro/compatibilidade/reversão no clone do ativo. Manter030000→040000→09/190000; QA já instalada, sem repetir. Nenhuma ativação autorizada. |
|
||
|
||
Provas: recibos `investor-ab-remaining-*`, `country-paths-*`, `country-search-chromium-20261010.json` e `form-entry-source-*`; detalhes e cobertura no relatório executivo e casos C88–C90. Pesquisa dos países/origem demonstrada em quatro formulários, sem repetir envios. Valores de estatísticas são datados:0→4 antes do quinto registo C90. Cinco registos são exclusivamente sintéticos QA, não conteúdos/importações de produção.
|
||
|
||
Mesma matriz3393requisitos/3393linhas/6845ocorrências, estados globais preservados. C37 autenticaçãoQA18 permanece fechado; não usar este lote para fechar auditoria integral ou decisões externas. Mercado Financeiro privado. Decisões pendentes concretas: calendário/responsáveis/suplentes para SLA interno; perfis modulares; quem restaura oportunidade arquivada e para qual estado. Preparação independente continua.
|
||
''')
|
||
section('casos-reproducao.md', 'qa-ab-countries-cases-20261010', '''## C88–C90 — corrigidos em QA, ativos não alterados
|
||
|
||
**C88 (P1; DESK02, GUIDE01, UI01):** abrir dossierA existente→Apoio no Balcão→pedido8e6511a8-c15d-46b7-8eb5-c34b6e31eb15. Esperado: reutilizar resumo guardado da ferramenta do mesmo projeto para preparar apoio, conservando o pedido. Observado: descrição vazia embora o rascunho financeiro atual tenha54bytes de resumo; B tinha54bytes recuperados. Causa identificada: preparação copiava uma vez; rascunho anterior não recuperava alterações posteriores e não havia ação explícita de reutilização. Não foi inferida a causa histórica da cópia vazia. Correção: prévia do resumo atual, botão explícito “Reutilizar descrição guardada e guardar rascunho”, só para descrição vazia, conta proprietária, request/version/source do mesmo dossier; servidor recusa fonte/versão divergente, sobrescrita e submissão conjunta; locks e histórico preservados. PUT pela interface guardou A,v3 da ferramenta, criou revisão do texto vazio e evento request_prefill_reused, sem novo pedido/conta/dossier. API/SQL e UI390/320 recuperam resumo/PT/ZA; gestor não recebe prévia financeira privada. Provas: investor-ab-remaining-* e regressão antes/depois. A tentativa de harness após guardar comparava versão textual com inteiro; a gravação não foi repetida, e a recuperação/revisão foram confirmadas depois. Reversão não apaga o histórico.
|
||
|
||
**C89 (P1; COUNTRY03/04, UI01):** nos quatro formulários públicos, consultar nomes acessíveis dos dois seletores. Esperado: país do requerente e origem declarada distinguíveis. Observado: ambos aria-label=País, apesar dos textos visuais diferentes. Causa: CountrySelect usava o label padrão para ambos. Correção: nomes explícitos “País do requerente” e “País de origem declarado desta solicitação” no formulário comum e na manifestação pública. Chromium comprovou seleção PT/ZA, gravação e recuperação/filtros; quatro exames390px sem violações automáticas/overflow. Leitor de ecrã real continua independente. Não foi alterado catálogo, significado ou dado histórico.
|
||
|
||
**C90 (P1; COUNTRY03/04; rastreabilidade de entrada, independente da origem-país):** enviar formulário público Investidor ou Empresa e recuperar registo na gestão/SQL. Esperado: guardar a página de entrada e apresentar país/origem como campos distintos. Observado: source_page=/apoio para ambos, porque o POST é partilhado. Causa: literal fixo no construtor InterestRequest. Correção: registar path de formulário conhecido somente com esquema/host/porta da mesma origem; fallback/endpoint=/apoio para referente externo, administrativo, inexistente ou outra porta. Queries pessoais não são guardadas. Replay conserva a origem da primeira receção. Interface da gestão mostra “Página de entrada registada”. Um envio novoC90→POST302→protocolo→gestão→/formularios/investidor/PT/ZA→SQLúnico, sem reatribuir os anteriores. Regressão antes falhava (/apoio); depois passa, incluindo replay e referentes não autorizados. Provas: form-entry-source-*qa.json e ab-country-final-regression-qa.json.
|
||
|
||
**Ensaios, sem falha técnica do portal:** rejeições422 dos emails sintéticos .invalid/example.com em manifestação pública são a validação DNS prevista; erroemail aparece e não grava pedido. Usado identificador sintético de domínio DNS aceite com notificaçõesQA desativadas. Não relaxar regra ou usar endereço como identidade real. Erros anteriores de harness (duas versões financeiras legítimas, caixa do estado draft e versão de revisão serializada como texto) preservados nos recibos de tentativa; não são duplicações ou falhas de autorização.
|
||
|
||
A/B internos agora concluídos em QA por composição de provas. C37 continua encerrado só na autenticaçãoQA18; cenários não afirmam submissão/decisão externa, auditoria integral ou ativação.
|
||
''')
|
||
section('plano-ativacao.md', 'qa-ab-countries-activation-20261010', f'''## Lote A/B e países C88–C90 — preparação, não ativação
|
||
|
||
QA demonstrada: A/B internos, países/origem por quatro formulários, recuperação de perfil/negócio/fundiário, filtros/estatística positiva e página de entrada. **0migrations,0tabelas novas,0contas novas neste lote.** Dados QA: reutilização do pedidoA existente com revisão/auditoria, quatro intakes países e um intake C90; não transferir fixtures para ativo. Resultados/drafts/tarefas A/B e originais preservados.
|
||
|
||
1. Fixar revisão candidata global depois dos critérios restantes por consumidor/perfil. Registar hashes das fontes/builds e dos recibos associados; fingerprint040 atual `{status['code_hash']}` é de âmbito próprio e não substitui fingerprint/revisão global do percurso.
|
||
2. Consultar ativo somente por leitura para diferenças reais de schema/flags/config/dados e instalações ausentes. O manifesto ativo `{status['active_manifest_sha256']}` continua intacto. Provas de backup/restauro anteriores conservam data/âmbito; não habilitam automaticamente a revisão nova.
|
||
3. Renovar backup/restauro ligado à candidata e ao ativo lido, testar extração/restauro dos ficheiros e permissões, compatibilidade e aplicação/reversão num cloneQA fresco. Só depois preparar a ação autenticada ativa, ainda fora da autorização.
|
||
4. Instalações necessárias continuam **recursos030000→perfis040000→percurso2026_10_09_190000**; não confundir190000 com Balcão07/190000. QA já instalada, não repetir. Antes do corte, prévias determinam contagens ausentes; não transportar identidades sintéticas nem escolher titulares reais. Bootstrap/designações continuam operações próprias.
|
||
5. Disponibilizar correções C88–C90 por código/build/config da candidata. Importação cartográfica por prévia/correspondências/hash/aplicação idempotente é outra ação, com173 pendências anteriores e ambiguidade Mandie/Guro preservadas; publicação das camadas/conteúdos e catálogo público é outra ação. `ready=true` indica preparação dentro do mecanismo, não instalação ou conclusão visual.
|
||
|
||
Compatibilidade/reversão do lote: usa description/source_page/eventos/revisions existentes, sem SQL de migração. Reverter somente os hunks/build deste incremento para revisão conhecida, preservando dados novos, revisões e auditoria; não fazer reset do repositório com alterações locais. Não reescrever/regredir descrições ou origens históricas. O texto reutilizado continua campo canónico; ações futuras devem reenviar versão atual. Reversão funcional global ainda deve ser ensaiada com a candidata/clone, sem declarar concluída por testes unitários. ClamD real anterior permanece separado das derivadas/operações não verificadas. C37 não será repetido para ativação deste lote.
|
||
''')
|
||
code_files = ['app/Services/VirtualDesk.php', 'app/Http/Controllers/VirtualDeskController.php', 'app/Http/Controllers/PortalServicesController.php',
|
||
'app/Http/Controllers/PortalManagementController.php', 'resources/js/pages/desk/request.tsx', 'resources/js/pages/portal/services.tsx',
|
||
'resources/js/pages/PortalInvestidor.tsx', 'resources/js/pages/management/interest.tsx', 'tests/Feature/InvestorPathTest.php', 'tests/Feature/PortalServicesTest.php']
|
||
scope = {'recorded_at': stamp.isoformat(), 'target': ab['target'], 'active_writes': 0, 'new_migrations': 0, 'new_tables': 0, 'new_accounts': 0,
|
||
'cases': ['C88', 'C89', 'C90'], 'ab_internal_scenarios': 'concluído e demonstrado em QA', 'requirements_touched': sorted(IDS),
|
||
'global_states_changed': False, 'global_candidate_fixed': False, 'activation_authorized': False,
|
||
'git_head_observed': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
|
||
'active_manifest_sha256': status['active_manifest_sha256'], 'profile_scoped_code_hash': status['code_hash'],
|
||
'qa_manifest_sha256': hashlib.sha256((ROOT / 'public/build-qa-opportunities/manifest.json').read_bytes()).hexdigest(),
|
||
'source_hashes': {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in code_files},
|
||
'receipt_hashes': {name: hashlib.sha256((PRIVATE / name).read_bytes()).hexdigest() for name in [
|
||
'investor-ab-chromium-20261010.json', 'investor-ab-remaining-chromium-20261010.json', 'investor-ab-remaining-persistence-qa.json',
|
||
'country-paths-chromium-20261010.json', 'country-paths-persistence-qa.json', 'country-search-chromium-20261010.json', 'form-entry-source-chromium-20261010.json',
|
||
'form-entry-source-persistence-qa.json', 'ab-country-final-regression-qa.json']}}
|
||
(OUT / 'scope-proof.json').write_text(json.dumps(scope, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
print(json.dumps({'prepared_tables': 2, 'requirements_touched': len(IDS), 'cases': scope['cases'], 'ab_internal_scenarios': scope['ab_internal_scenarios'], 'active_writes': 0}, ensure_ascii=False))
|