Files
Investidor-V2/tools/portal-segmentation-reconcile.py
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

240 lines
24 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.
"""Prepare additions to the SAME registries; preserve all prior criteria and states."""
from pathlib import Path
from datetime import datetime, timezone
from collections import Counter, defaultdict
from zipfile import ZipFile
import csv, hashlib, json, re, xml.etree.ElementTree as ET
csv.field_size_limit(20_000_000)
root = Path(__file__).resolve().parent.parent
private = root/'storage/app/private/investmoz-v2/portal-audit'
directory = private/'segmentation-reconciliation-20261011'
docs = root/'docs/investmoz/v2/auditoria-portal'
extraction = json.loads((directory/'extraction.json').read_text(encoding='utf-8'))
provenance = json.loads((directory/'source-provenance.json').read_text(encoding='utf-8'))
sha = lambda data: hashlib.sha256(data).hexdigest()
digest = provenance['sha256']
tag = '[Segmentação '+digest[:12]+' — exemplar v1 local; identidade v1(3) pendente] '
matrix_rows = list(csv.reader((docs/'matriz-requisitos.csv').open(encoding='utf-8-sig', newline='')))
headers = matrix_rows[0]
existing = {r[headers.index('ID')]: dict(zip(headers, r)) for r in matrix_rows[1:]}
new = {
'AUD-MARKET-01': ('Negócios similares por área, raio, sector e classificação, com fonte, data e cobertura', 'não implementado', 'INV-004',
'Reutilizar ExistingBusiness/objetos e geo_locations/geo_geometries para estabelecimentos comprovados. Separar estabelecimento observado de anúncio de transação; pesquisa espacial explicada, sem transformar artigo ou licença em oportunidade.',
'Sem consumidor de negócios similares por raio/classificação demonstrado. O marketplace de negócios não comprova um cadastro de estabelecimentos.',
'Pesquisa combina raio/sector/classificação; apresenta fonte/data/cobertura/limitações; recupera os mesmos registos; não mostra dados privados nem locais sem correspondência.'),
'AUD-ESTABLISHMENT-01': ('Importar empresas licenciadas e empreendimentos turísticos com normalização, correspondências e rejeições, sem duplicações', 'não implementado', 'INV-008',
'Inventariar os ficheiros fornecidos, inspecionar colunas e IDs reais; reutilizar organizações/estabelecimentos existentes por correspondência, serviços de importação privada/hash e hierarquia territorial. Não criar organizações automaticamente com base em nome ou licença.',
'Importador específico destes cadastros e reconciliação de entidades/versões ainda por preparar. Importação de artigos do legado e geometrias não substitui esta carga.',
'Prévia sem escrita; casos inequívocos/casos ambíguos/rejeições por linha; aplicação QA com ator autorizado; repetição sem duplicações; fonte/data/história e recuperação; nenhuma georreferenciação inventada.'),
'AUD-FUND-01': ('Catálogo de fundos nacionais e territoriais com elegibilidade explicada e atualização', 'não implementado', 'INV-005',
'Reutilizar biblioteca e cadastro de entidades/parceiros para fontes e gestores reais; regras versionadas de cobertura/elegibilidade com indeterminado quando faltar parâmetro. Correspondência não representa aprovação ou compromisso de financiamento.',
'Correspondência financeira genérica RF-CLU-005 é relacionada, mas não implementa o catálogo de fundos com cobertura e janelas. Não foram encontrados modelo/controlador/consumidor específicos no inventário de código.',
'Fonte, gestor, sectores, cobertura nacional ou territorial, datas/estado e documentos; filtro e explicação por critério; fundos expirados sinalizados; guardar interesse autorizado sem duplicação.'),
'AUD-COMPANY-01': ('Comparador de forma societária orientado por perguntas, com fundamento, alternativas e limites', 'não implementado', 'SOC-001',
'Reutilizar organização mestre, perfil/dossier e biblioteca. Comparador informativo por número de participantes, responsabilidade, gestão, capital e atividade; separar empresário individual, sociedade, representação e cooperação.',
'Seleção de forma jurídica no roteiro não comprova comparador orientado por perguntas. Termos e pressupostos da secção9 devem ser reconciliados com o exemplar legal aplicável antes de se tornarem regras.',
'Perguntas recuperáveis, alternativas explicadas com fonte/data/limites, correção dos dados, retorno ao dossier sem alterar retrospectivamente a entidade ou garantir decisão externa.'),
'AUD-COMPANY-02': ('Modelos versionados de estatutos e contratos sociais por forma jurídica, com finalidade e limitações', 'não implementado', 'SOC-002',
'Reutilizar documents/portal_resources/revisions e links, sem outro repositório; conservar original, hash, origem, versão, tipo, finalidade e adaptação separada.',
'A secção6.1 afirma disponibilidade ativa parcial, mas não identifica exemplares e URLs. A biblioteca genérica não comprova estes modelos; não marcar ativos sem localizar os ficheiros e verificar recuperação/permissões.',
'Exemplar/URL exatos por modelo, original recuperável, versão e limitações visíveis, formulário contextual e derivada própria; nova versão conserva submissões/modelos anteriores.'),
'AUD-CAPITAL-01': ('Orientação sobre private equity e capitalização, preparação, riscos, documentos e percurso', 'não implementado', 'CAP-001',
'Reutilizar biblioteca/guia público, dossier e parceiros autorizados; manter separado de ofertas experimentais. RF-CRM-009 e regras de facilitação são relações existentes, sem equivalência automática com este guia.',
'Não foi demonstrado guia estruturado de capitalização. O pedido não autoriza custódia, intermediação, promessa de retorno ou aprovação dos módulos de oferta.',
'Consulta pública com fontes/versões, instrumentos distinguidos, riscos/documentos e próxima ação; guardar referência no dossier correto; sem recomendar sucesso nem assumir compromisso.'),
'AUD-CAPITAL-02': ('Guião de admissão à Bolsa de Valores de Moçambique, distinguindo Bolsa, regulador e operadores', 'não implementado', 'CAP-002',
'Reutilizar guia/biblioteca/dossier/checklists. Relacionar pedido de admissão e patrocínio às referências específicas; separar ação interna do portal e ato de entidade competente.',
'Guião por segmento, documentos, condições e fonte aplicável ainda não demonstrado. Não inferir valores, prazos, prospectos, taxas ou aprovação pelos artigos1–3.',
'Etapas, entidade, base/artigo/redação, documentos, requisitos por segmento e lacunas explícitas; recuperação no dossier e encaminhamento permitido, sem integração externa simulada.'),
'AUD-CHECKLIST-01': ('Editor de checklists e formulários configuráveis, publicação de versões e preservação de submissões anteriores', 'parcial', 'INV-007',
'Reutilizar config/document-requirements.php, requisitos/formulários/documentos/tarefas e serviços do Balcão existentes. Não duplicar motor, tabelas ou as cinco fases/19 etapas.',
'Checklists e catálogos configurados existentes; editor sem alterar código/publicação versionada por entidade não demonstrados. Separar obrigações comerciais, registo/publicidade e licença setorial.',
'Gestor autorizado configura/previsualiza/publica versão, investidor recebe versão aplicável ao contexto/data, guarda incompleto e recupera; nova versão não modifica submissão anterior nem trata decisão interna como licença.'),
}
assert not (set(new)&set(existing)), 'New ID already belongs to the canonical matrix'
aliases = {
'INV-001':['MASTER-1f0a4cde-0001','AUD-DESK-02'], 'INV-002':['AUD-DESK-01','AUD-CRM-01'],
'INV-003':['AUD-SIM-01','AUD-SIM-02'], 'INV-004':['AUD-MARKET-01'], 'INV-005':['AUD-FUND-01'],
'INV-006':['AUD-OP-03','AUD-PROFILE-28'], 'INV-007':['AUD-CHECKLIST-01','MASTER-9dee08d0-0049'],
'INV-008':['AUD-ESTABLISHMENT-01'], 'INV-009':['AUD-PROFILE-11','AUD-PROFILE-19','AUD-PROFILE-21'],
'INV-010':['MASTER-e1169a77-0018'], 'SOC-001':['AUD-COMPANY-01'], 'SOC-002':['AUD-COMPANY-02'],
'CAP-001':['AUD-CAPITAL-01'], 'CAP-002':['AUD-CAPITAL-02'],
}
all_ids = set(existing)|set(new)
assert all(rid in all_ids for ids in aliases.values() for rid in ids)
def section_ids(key):
exact = {
'1.2':aliases['INV-001']+aliases['INV-007'], '1.3':aliases['INV-003'],
'1.4':aliases['INV-006']+aliases['CAP-001']+aliases['CAP-002'],
'1.5':aliases['INV-004']+aliases['INV-005']+aliases['INV-008'],
'1.6':aliases['INV-007']+aliases['INV-009'], '1.7':aliases['INV-007']+aliases['INV-009'],
'3.1':['AUD-PROFILE-01','AUD-PROFILE-09','AUD-PROFILE-12'],
'3.1.1':['AUD-PROFILE-06','AUD-PROFILE-07','AUD-PROFILE-17','AUD-PROFILE-24'],
'3.2':aliases['INV-001']+aliases['INV-002'], '3.2.1':['AUD-PROFILE-08','V2-12','AUD-DESK-02'],
'3.2.2':['AUD-OP-02','AUD-CHECKLIST-01','AUD-PROFILE-25'],
'3.2.3':['AUD-OP-04','AUD-MARKET-01','AUD-FUND-01'],
'3.3':aliases['INV-006'], '3.3.1':['AUD-OP-02','AUD-OP-03','AUD-PROFILE-28'],
'4.3':['AUD-GEO-03','AUD-ESTABLISHMENT-01','RF-CRM-001'],
'4.4':['AUD-CHECKLIST-01','AUD-PROFILE-01','AUD-OPS-03'],
'5.2':['AUD-DESK-02','V2-12','AUD-PROFILE-19'], '5.3':['AUD-OP-02','AUD-OPS-01'],
'6.2':['AUD-OP-02','AUD-CHECKLIST-01','AUD-PROFILE-08'],
'6.3':['AUD-OP-03','AUD-PROFILE-01','AUD-LIB-01'], '6.3.1':['AUD-CHECKLIST-01','AUD-LIB-01'],
'7.1':['AUD-PROFILE-26','RF-CRM-011'], '7.2':['AUD-LIB-02','AUD-SCAN-01'],
'7.3':['AUD-LIB-01','AUD-SIM-02','AUD-ESTABLISHMENT-01'],
'8.1':['AUD-ESTABLISHMENT-01','AUD-LIB-02'], '8.2':['AUD-UI-01','AUD-PROFILE-30','AUD-OPS-03'],
'8.4':['AUD-PROFILE-20','AUD-PROFILE-22'], '8.6':aliases['INV-010'],
'9.3':aliases['SOC-002'],
}
if key in exact:return exact[key]
if key.startswith(('1.0','1.8')):return ['AUD-CHECKLIST-01','AUD-LIB-01','AUD-SIM-02']
if key.startswith('9'):return aliases['SOC-001']+aliases['SOC-002']
if key.startswith('4'):return ['AUD-OPS-03','AUD-GEO-03']
if key.startswith('5'):return ['AUD-DESK-02','AUD-SIM-02','AUD-OP-03']
if key.startswith('7'):return ['AUD-PROFILE-23','AUD-PROFILE-26','AUD-OPS-01']
if key.startswith('2.1'):return ['AUD-UI-01','AUD-OPS-03','MASTER-e1169a77-0018']
return ['AUD-OPS-01','AUD-OPS-03']
# Table cells are grouped by semantic row. Header rows and section headings are not requests.
paragraphs = extraction['paragraphs']
tables = defaultdict(list)
for p in paragraphs:
if 'table' in p:tables[(p['table'],p['row'])].append(p)
blocks = []
used_tables = set()
for p in paragraphs:
if 'table' in p:
key = (p['table'],p['row'])
if key in used_tables:continue
used_tables.add(key)
if p['row']==1:continue
members = tables[key]
text = ' | '.join(n['text'] for n in members if n['text'])
ids = sorted({rid for alias in re.findall(r'\b(?:INV-\d{3}|SOC-\d{3}|CAP-\d{3})\b',text) for rid in aliases.get(alias,[])})
ids = ids or section_ids(p['section'])
else:
if not p['text'] or p['style'].startswith(('Ttulo','Heading','Title','Subtitle')):continue
members=[p]; text=p['text'];ids=section_ids(p['section'])
assert all(rid in all_ids for rid in ids)
blocks.append({'id':'SEG-'+digest[:8]+'-p'+str(members[0]['paragraph']), 'section':p['section'],
'first_paragraph':members[0]['paragraph'],'last_paragraph':members[-1]['paragraph'],
'text':text,'requirements':sorted(set(ids)), 'source_sha256':digest,
'locator':'word/document.xml#p'+str(members[0]['paragraph']),
'declared_document_state_is_not_functional_evidence':True,
'exact_requested_exemplar_confirmed':False})
by_id=defaultdict(list)
for block in blocks:
for rid in block['requirements']:by_id[rid].append(block)
correspondence=[]
for section in extraction['sections']:
scoped=[b for b in blocks if b['section']==section['key']]
ids=sorted({rid for b in scoped for rid in b['requirements']})
if not ids:ids=section_ids(section['key'])
correspondence.append({'section':section['key'],'heading':section['heading'], 'paragraphs':section['paragraphs'],
'requirements':ids,'states':{rid:existing[rid]['Estado'] if rid in existing else new[rid][1] for rid in ids},
'disposition':{rid:'reutilizado/complementado' if rid in existing else 'acrescentado sem correspondência direta' for rid in ids},
'next_action':{rid:existing[rid]['Lacuna'] if rid in existing else new[rid][4] for rid in ids},
'requested_exemplar_confirmation_pending':True})
# The legal fields are statements attributed to this specification, never activated law.
legal=[]
for start in range(338,406,4):
ps=paragraphs[start-1:start+3]
if len(ps)!=4:continue
legal.append({'reference':ps[0]['text'],'locator':ps[0]['locator'],'source_sha256':digest,
'access_declared_by_document':ps[1]['text'],'amendments_repeals_declared_by_document':ps[2]['text'],
'applicable_redaction_declared_by_document':ps[3]['text'],
'urls':sorted({link['url'] for p in ps for link in p['hyperlinks'] if link['url']}),
'linked_requirements':section_ids('1.8.4'),'official_acceptance_preserved':True,
'portal_applicability_evidence_not_inferred':True,'rule_enabled':False})
legal.append({'reference':'Verificação técnica adicional de três URLs, 11/10/2026',
'access':'PDF CMVM no Banco de Moçambique recuperado pela ferramenta web (28 páginas); INTIC BR229 indisponível via ferramenta e BVM timeout.',
'amendments_repeals':'Não foi realizada pesquisa exaustiva nova; conservar a atribuição às declarações do exemplar.',
'applicable_redaction':'Nenhuma redação foi adotada no motor; artigo/âmbito/período continuam por regra concreta.',
'official_acceptance_preserved':True,'rule_enabled':False})
packet={'proof_filename':'csv-authoring-proof.json','files':[]}
for name in ['matriz-requisitos.csv','backlog-priorizado.csv','inventario-fontes.csv']:
raw=(docs/name).read_bytes()
rows=list(csv.reader((docs/name).open(encoding='utf-8-sig',newline='')));hs=rows[0];patches=[];append=[]
if name=='matriz-requisitos.csv':
for i,row in enumerate(rows[1:],1):
rid=row[hs.index('ID')]
if rid not in by_id:continue
before=dict(zip(hs,row));assert tag not in before['Fonte/ocorrências']
origin=json.loads(before['Critérios e versões por origem (JSON)'] or '[]')
assert isinstance(origin,list)
references=[{'id':b['id'],'source':provenance['source'],'sha256':digest,'section':b['section'],
'locator':b['locator'],'first_paragraph':b['first_paragraph'],'last_paragraph':b['last_paragraph'],
'text':b['text'],'document_state_not_acceptance':True,'requested_exemplar_identity_pending':True} for b in by_id[rid]]
values={'Fonte/ocorrências':before['Fonte/ocorrências']+'\n'+tag+'; '.join(b['id']+' §'+b['section'] for b in by_id[rid]),
'IDs das linhas de origem':before['IDs das linhas de origem']+'; '+'; '.join(b['id'] for b in by_id[rid]),
'Quantidade de linhas de origem':str(int(before['Quantidade de linhas de origem'])+len(references)),
'Critérios e versões por origem (JSON)':json.dumps(origin+references,ensure_ascii=False),
'Versões divergentes':before['Versões divergentes']+'\n'+tag+'Fases0–4 do §2 são releases de desenvolvimento, não as cinco fases/19 etapas. Agrupamentos §3.3.1 não convertem categorias. Papéis genéricos §3.1 não concedem permissão. §6 contém estados documentais, não prova do ambiente atual. §7p811 não reabre credibilidade das fontes oficiais. SLA/ofertas/competências continuam decisões separadas. Disponibilidade do guia/modelos só por URL/exemplar verificado.',
'Consolidação':before['Consolidação']+'\n'+tag+'Pedidos relacionados sem novo inventário/backlog paralelo; referência documental acrescentada, critérios anteriores preservados. Confirmar os bytes do v1(3) antes do fecho documental.'}
if rid in ['AUD-LIB-01','AUD-LIB-02']:
proof=json.loads((directory/'library-replay-qa.json').read_text(encoding='utf-8'))
values['Evidência funcional']=before['Evidência funcional']+'\n'+tag+'Prévia→importação privada QA por gestor17→ClamD real→repetição sem duplicar→SHA/bytes originais recuperados. Recurso29/documento589ed9ac-967e-439e-9dde-b0fb3996e546. Chromium gestão/download autorizado e nova sessão; Promotor403 e público404. Recibos library-preview/apply/replay-qa.json e segmentation-library-chromium-20261011.json. Não publicar ou ativar regras.'
patches.append({'row':i,'values':values})
for rid,(request,state,alias,reuse,gap,acceptance) in new.items():
refs=by_id[rid]
values={h:'' for h in hs}
values.update({'ID':rid,'Pedido/trecho':request,'Fonte/ocorrências':tag+'Alias documental '+alias+'; '+'; '.join(b['id'] for b in refs),
'Módulo':alias+' / incremento posterior ao workflow das seis categorias',
'Rotas':'Consumidor específico ainda por delimitar; reutilizar rotas de biblioteca/Balcão/negócios/simulador pertinentes, sem atribuir existência fictícia.',
'Implementação encontrada':reuse,'Disponibilidade ativa':'Não demonstrada para este requisito específico; nenhum pacote ativado por esta incorporação. COUNTRY02ON preservado.',
'Evidência funcional':'Leitura integral do exemplar local e inventário código/documental; importação/retrieval deste DOCX não comprova a funcionalidade descrita.',
'Estado':state,'Lacuna':gap,'Dependência':'Confirmar exemplar solicitado v1(3). '+('Identificar fontes/dados/âmbito/período concretos; não reabrir aceitação oficial.' if rid not in ['AUD-CAPITAL-01','AUD-CHECKLIST-01'] else 'Reutilizar componentes/contratos; definição funcional própria e nenhuma nova ativação.'),
'Critério de aceitação':acceptance,'Prioridade':'P2','Versões divergentes':'Código '+alias+' é alias deste documento, não renumeração do roteiro/backlog. Conteúdo/regras pendentes não se tornam aprovados pela importação.',
'IDs das linhas de origem':'; '.join(b['id'] for b in refs),'Quantidade de linhas de origem':str(len(refs)),
'Critérios e versões por origem (JSON)':json.dumps(refs,ensure_ascii=False),
'Interface':'Consumidor específico não demonstrado.','API':'Contrato específico não demonstrado.',
'Gravação':'Nenhuma entidade/regra criada para este módulo.','Recuperação':'A recuperação do original da especificação é prova do acervo, não deste percurso.',
'Resultado apresentado':'Pendente de implementação/prova própria.','Consolidação':'Novo ID apenas após pesquisa de correspondência: os requisitos relacionados são componentes a reutilizar, não equivalências. '+tag})
append.append([values[h] for h in hs])
elif name=='backlog-priorizado.csv':
for i,row in enumerate(rows[1:],1):
rid=row[hs.index('ID')]
if rid not in by_id:continue
old=dict(zip(hs,row))
patches.append({'row':i,'values':{'Correção/componentes':old['Correção/componentes']+'\n'+tag+'§'+', §'.join(sorted({b['section'] for b in by_id[rid]}))+'; referência e critérios complementares na matriz existente, sem repetir provas.',
'Dependência/pergunta':old['Dependência/pergunta']+'\n'+tag+'Confirmar identidade do exemplar solicitado. Não reabrir A/B/C37/COUNTRY02; ofertas/competências/SLA e restauro proposto continuam separados.'}})
for rid,(request,state,alias,reuse,gap,acceptance) in new.items():
data={'Prioridade':'P2','ID':rid,'Estado':state,'Pendência':gap,'Correção/componentes':reuse,
'Dependência/pergunta':'Exemplar v1(3) ainda não confirmado; fontes/dados/atos concretos próprios. Nenhuma nova regra ou divulgação inferida.',
'Aceitação':acceptance,'Ordem do próximo lote':'Incremento separado do workflow base; '+alias+' é alias documental. '+tag}
append.append([data[h] for h in hs])
else:
known={(r[hs.index('Exemplar/caminho')],r[hs.index('SHA256')]) for r in rows[1:]}
for item in provenance['source_directory_inventory']:
if (item['path'],item['sha256']) in known:continue
data={'Exemplar/caminho':item['path'],'Bytes':str(item['bytes']),'SHA256':item['sha256'],'Tipo':Path(item['path']).suffix,
'Nota de leitura':('Original integral907parágrafos/14tabelas; relações externas/notas/alterações verificadas; hash e cópia privada preservados. Identidade v1(3) pendente.' if item['sha256']==digest else 'Inventariado integralmente no mesmo diretório antes da seleção; conteúdo não promovido a implementação. '+item.get('read_error','')),
'Disponibilidade':'PrivadoQA/biblioteca recurso29' if item['sha256']==digest else 'Origem acessível; divulgação não inferida.',
'Uso em cálculo':'Nenhum cálculo ativado.','Situação jurídica/documental':'Documento de programação; não estender aceitação oficial. Original/histórico preservados.',
'Edição/versão identificada':'v1; propriedadeOOXMLrevision15; modificado11/10/2026 12:29UTC; v1(3) pendente.' if item['sha256']==digest else 'Nome/hash do exemplar; versão substantiva não inferida.'}
append.append([data[h] for h in hs])
for attachment in item['attachments']:
data={h:'' for h in hs};data.update({'Exemplar/caminho':item['path']+'::'+attachment['part'],'Bytes':str(attachment['bytes']),'SHA256':attachment['sha256'],'Tipo':'anexo incorporado','Nota de leitura':'Anexo inventariado sem descarte; origem no DOCX indicado.','Disponibilidade':'Parte do original, divulgação não inferida.','Uso em cálculo':'Nenhum cálculo ativado.','Situação jurídica/documental':'Não inferida.','Edição/versão identificada':item['sha256']})
if (data['Exemplar/caminho'],data['SHA256']) not in known:append.append([data[h] for h in hs])
packet['files'].append({'filename':name,'input_sha256':sha(raw),'rows':rows,'patches':patches,'append':append,
'source_hash_updates':[],'allowed_columns':sorted({k for p in patches for k in p['values']})})
assert len(blocks)==len({b['id'] for b in blocks})
assert sum(len(v) for v in by_id.values())>=len(blocks)
states=Counter(x['Estado'] for x in existing.values())+Counter(v[1] for v in new.values())
counts={'old_requirements':len(existing),'new_requirements':len(new),'requirements':len(existing)+len(new),
'matrix_rows':len(existing)+len(new),'source_occurrences_before':sum(int(x['Quantidade de linhas de origem']) for x in existing.values()),
'added_source_occurrences':sum(len(v) for v in by_id.values()),'source_request_blocks':len(blocks),
'sections':len(correspondence),'states':dict(states),'reused_requirement_IDs':sorted(set(by_id)-set(new)),
'new_IDs':sorted(new),'no_old_state_changed':True,'closed_AB_C37_preserved':True,'active_writes':0}
counts['source_occurrences']=counts['source_occurrences_before']+counts['added_source_occurrences']
for name,value in [('csv-edits.json',packet),('correspondence.json',correspondence),('request-blocks.json',blocks),
('document-aliases.json',aliases),('legal-reference-links.json',legal),('counts.json',counts)]:
(directory/name).write_text(json.dumps(value,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({k:counts[k] for k in ['old_requirements','new_requirements','requirements','source_request_blocks','sections','added_source_occurrences','active_writes']},ensure_ascii=False))