229 lines
23 KiB
Python
229 lines
23 KiB
Python
"""Read the supplied source; prepare existing catalogue references, never live data."""
|
||
from pathlib import Path
|
||
import argparse, hashlib, json, re, unicodedata, zipfile
|
||
import xml.etree.ElementTree as ET
|
||
|
||
parser=argparse.ArgumentParser()
|
||
parser.add_argument('source', type=Path)
|
||
parser.add_argument('--output', type=Path, required=True)
|
||
args=parser.parse_args()
|
||
assert args.source.name == 'Instituições financeiras em Moçambique.docx'
|
||
ns={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
||
raw=args.source.read_bytes();digest=hashlib.sha256(raw).hexdigest()
|
||
assert digest=='119bb5514d2777849b70aebfec21d28a41885cbdc7d9827fbebd764fe994c108', 'Reconcile a changed source before preparing another edition'
|
||
def clean(s): return re.sub(r'\s+',' ',s).strip()
|
||
def norm(s): return re.sub(r'[^a-z0-9]+',' ',unicodedata.normalize('NFKD',s).encode('ascii','ignore').decode().lower()).strip()
|
||
def slug(s): return norm(s).replace(' ','-')[:75]
|
||
with zipfile.ZipFile(args.source) as z:
|
||
body=ET.fromstring(z.read('word/document.xml')).find('w:body',ns)
|
||
tables={};paragraphs=[];heading='';number=0
|
||
for node in body:
|
||
if node.tag.endswith('}p'):
|
||
text=clean(''.join(node.itertext()))
|
||
if not text or 'PAGEREF' in text or text.startswith('TOC '): continue
|
||
if re.match(r'^\d+\.',text): heading=text
|
||
paragraphs.append((heading,text))
|
||
elif node.tag.endswith('}tbl'):
|
||
number+=1
|
||
tables[number]=(heading,[[clean(' '.join(c.itertext())) for c in r.findall('w:tc',ns)] for r in node.findall('w:tr',ns)])
|
||
base=json.loads((Path(__file__).resolve().parents[1]/'database/data/funding-references.json').read_text(encoding='utf-8'))
|
||
refs={};coverage=[]
|
||
category={'commercial_bank':'banking','investment_bank':'banking','bank':'banking','microbank':'credit','cooperative':'credit','savings':'credit','investment_manager':'investment','investment_company':'investment','pension_manager':'pensions','insurer':'insurance','reinsurer':'insurance','microinsurer':'insurance','broker':'intermediation','agent':'intermediation','reinsurance_broker':'intermediation','exchange_house':'exchange','electronic_money':'payments','interbank':'payments','supervisor':'supervision','other':'other'}
|
||
services={'banking':['investment','working_capital','guarantee','payments'],'credit':['investment','working_capital'],'investment':['investment_management','equity'],'pensions':['pension'],'insurance':['insurance'],'intermediation':['insurance'],'exchange':['foreign_exchange'],'payments':['payments'],'supervision':['other'],'other':['other']}
|
||
def key(name):
|
||
n=norm(name)
|
||
if 'gestora' in n and 'standard bank' in n: return 'standard-bank-pension-manager'
|
||
if 'nbc' in n and ('sgfp' in n or 'pensoes' in n): return 'nbc-pension-manager'
|
||
if 'letshego' in n and 'financial services' in n: return 'letshego-microbank-historical'
|
||
patterns=[('standard bank','standard-bank'),('millennium','millennium-bim'),('bci','bci'),('moza banco','moza'),('banco nacional de investimento','bni'),('bni','bni'),('gapi','gapi'),('bfs','bfs'),('socremo','socremo'),('banco letshego','letshego-bank'),('letshego','letshego-bank'),('ac microbanco','ac-microbanco'),('xtenda','xtenda'),('yingwe microbanco','yingwe-microbanco'),('my bucks','my-bucks'),('mybucks','my-bucks'),('microbanco confianca','confianca-microbanco'),('caixa financeira de catandica','caixa-catandica'),('caixa financeira de caia','caixa-caia'),('futuro microbanco','futuro-microbanco'),('access bank','access-bank'),('african banking corporation','abc-historical'),('banco big','banco-big'),('first capital','first-capital-bank'),('ecobank','ecobank'),('nedbank','nedbank'),('uba','uba'),('vista bank','vista-bank'),('fnb','fnb'),('absa','absa'),('issm','issm'),('global alliance','global-alliance'),('fidelidade','fidelidade'),('sanlam mocambique vida','sanlam-vida'),('mocambique companhia de seguros','mcs-seguradora'),('empresa mocambicana de seguros','emose'),('emose','emose'),('emergitus','emeritus'),('emeritus','emeritus'),('companhia de seguros indico','indico'),('britam','britam'),('arko companhia','arko'),('palma companhia','palma'),('hollard vida','hollard-vida'),('hollard mocambique','hollard-mocambique'),('confianca micro seguros','confianca-microseguros'),('caixa das mulheres de nacala','women-nacala-credit-cooperative'),('cooperativa de credito das mulheres de nacala','women-nacala-credit-cooperative'),('sociedade cooperativa de credito das mulheres de nacala','women-nacala-credit-cooperative')]
|
||
for needle,value in patterns:
|
||
if n==needle or n.startswith(needle+' ') or needle in n and needle in ['sociedade cooperativa de credito das mulheres de nacala','empresa mocambicana de seguros','banco nacional de investimento']:return value
|
||
n=re.sub(r' (?:s a|sa|sarl|scrl|lda|ei|su)$','',n)
|
||
if 'cooperativa' in n and 'pemba' in n and 'mulheres' in n:return 'women-pemba-credit-cooperative'
|
||
return 'fin-'+slug(n)
|
||
def nature(t):
|
||
return 'bank' if t in ['commercial_bank','investment_bank','bank'] else 'microfinance' if t in ['microbank','cooperative','savings'] else 'other'
|
||
def evidence(table,row,text):return {'id':digest[:16]+f':t{table}:r{row}','section':tables[table][0],'text':clean(text)}
|
||
def ensure(name,t,identity=None):
|
||
existing=identity or key(name)
|
||
if len(existing)>80:existing=existing[:67]+'-'+hashlib.sha256(existing.encode()).hexdigest()[:12]
|
||
if t is None: t=refs[existing]['draft']['institution_types'][0] if existing in refs else 'other'
|
||
k=existing
|
||
if k not in refs:
|
||
if k in base['references'] and base['references'][k]['draft']['kind']=='institution': ref=json.loads(json.dumps(base['references'][k]))
|
||
else:
|
||
ref={'edition':'institutions-nov2026-v1','provenance':[], 'draft':{'title':name,'kind':'institution','summary':'Instituição identificada no documento fornecido. Categoria, localização e contactos são informativos; produtos, disponibilidade e condições não documentados permanecem por confirmar.','manager_name':name,'source_url':None,'source_reference':args.source.name+' — Novembro de 2026','source_checked_on':'2026-11-10','source_status':'identified','coverage':'unknown','coverage_text':'Cobertura dos serviços/financiamento por confirmar. A presença física não determina a cobertura.','territory_ids':[],'sectors_scope':'unknown','sector_ids':[],'resource_ids':[],'criteria':[],'windows':[],'institution_key':k,'institution_nature':nature(t),'contacts':[],'locations':[],'products':[]}}
|
||
d=ref['draft'];d.update({'source_document_name':args.source.name,'source_document_sha256':digest,'institution_categories':[],'institution_types':[],'institution_services':[],'institution_status':'reported','aliases':[],'source_rows':[]})
|
||
ref['edition']='institutions-nov2026-v1'
|
||
ref['provenance'].append({'reference':args.source.name+'; edição Novembro de 2026; afirmações e limites do original conservados','sha256':digest,'checked_on':'2026-11-10'})
|
||
refs[k]=ref
|
||
d=refs[k]['draft']
|
||
for field,value in [('institution_types',t),('institution_categories',category[t])]:
|
||
if value not in d[field]: d[field].append(value)
|
||
for service in services[category[t]]:
|
||
if service not in d['institution_services']:d['institution_services'].append(service)
|
||
if name not in d['aliases']:d['aliases'].append(name)
|
||
return k
|
||
def attach(k,e):
|
||
if e not in refs[k]['draft']['source_rows']:refs[k]['draft']['source_rows'].append(e)
|
||
def origin(text):
|
||
n=norm(text)
|
||
if any(x in n for x in ['confirmar','directorio','diretorio','nao encontrada','nao encontrado','nao obtida','nao confirmad','nao e a mesma','nao confirmei']):return 'to_confirm'
|
||
if any(x in n for x in ['antiga','2021','2023','historica']):return 'historical'
|
||
return 'reported'
|
||
def province(text):
|
||
n=norm(text)
|
||
pairs=[('maputo provincia','Maputo Província'),('provincia de maputo','Maputo Província'),('cidade de maputo','Maputo Cidade'),('maputo cidade','Maputo Cidade'),('matola','Maputo Província'),('marracuene','Maputo Província'),('boane','Maputo Província'),('matutuine','Maputo Província'),('gaza','Gaza'),('xai xai','Gaza'),('inhambane','Inhambane'),('maxixe','Inhambane'),('morrumbene','Inhambane'),('sofala','Sofala'),('beira','Sofala'),('manica','Manica'),('chimoio','Manica'),('catandica','Manica'),('tete','Tete'),('angonia','Tete'),('zambezia','Zambezia'),('quelimane','Zambezia'),('nampula','Nampula'),('nacala','Nampula'),('niassa','Niassa'),('lichinga','Niassa'),('cuamba','Niassa'),('cabo delgado','Cabo Delgado'),('pemba','Cabo Delgado'),('montepuez','Cabo Delgado'),('maputo','Maputo Cidade')]
|
||
for needle,target in pairs:
|
||
if needle in n:return target
|
||
return None
|
||
def source(e,state):return {'source_url':None,'source_reference':args.source.name+' — '+e['section']+'; '+e['id']+'; fonte declarada no original','source_document_name':args.source.name,'source_document_sha256':digest,'checked_on':'2026-11-10','evidence_status':state,'origin':'Documento fornecido; não é uma nova verificação do regulador','source_refs':[e['id']]}
|
||
def location(k,label,address,place,e,role='branch',state=None,hours=''):
|
||
d=refs[k]['draft'];state=state or origin(address+' '+place)
|
||
if not address or any(x in norm(address) for x in ['nao encontrad','nao obtid','moradas nao confirmad']):state='unlocated'
|
||
identity=hashlib.sha256((k+'|'+role+'|'+norm(label)+'|'+norm(address)+'|'+norm(place)).encode()).hexdigest()[:24]
|
||
for old in d['locations']:
|
||
if old.get('id')==identity:
|
||
if e['id'] not in old['source_refs']:old['source_refs'].append(e['id'])
|
||
return
|
||
loc={**source(e,state),'id':identity,'role':role,'label':label[:250],'address':address[:1500],'territory_id':None,'location_text':place[:1000],'hours':hours[:1000]}
|
||
d['locations'].append(loc)
|
||
p=province(place)
|
||
if p:refs[k].setdefault('location_territory_names',{})[str(len(d['locations'])-1)]=p
|
||
def contacts(k,text,e,state=None):
|
||
if not text or norm(text) in ['nao encontrado','nao encontrada','nao obtido','nao encontrada nas 115 entradas lidas','']:return
|
||
d=refs[k]['draft'];state=state or origin(text)
|
||
emails=re.findall(r'[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}',text)
|
||
values=[('email',v) for v in emails]
|
||
# Preserve the exact business telephone assertion, including qualifiers; do not fabricate missing digits.
|
||
if re.search(r'\d{2}[\s\d()+/;-]{5,}',text):values.append(('phone',text[:1000]))
|
||
for kind,value in values:
|
||
identity=hashlib.sha256((k+'|'+kind+'|'+norm(value)).encode()).hexdigest()[:24]
|
||
if not any(c.get('id')==identity or c['kind']==kind and c['value']==value for c in d['contacts']):d['contacts'].append({**source(e,state),'id':identity,'kind':kind,'value':value})
|
||
def add(table,row,k,e):coverage.append({'table':table,'row':row,'institutions':[k],'source_row':e['id'],'status':'represented'});attach(k,e)
|
||
def typ(text):
|
||
n=norm(text)
|
||
for part,result in [('gestora de fundos de investimento','investment_manager'),('gestora de fundos de pensoes','pension_manager'),('cooperativa','cooperative'),('microbanco','microbank'),('microbancos','microbank'),('micro segur','microinsurer'),('supervisor','supervisor'),('investimento publico','investment_bank'),('banco de investimento','investment_bank'),('banco comercial','commercial_bank'),('moeda electronica','electronic_money'),('interbancarios','interbank'),('sociedade de investimento','investment_company')]:
|
||
if part in n:return result
|
||
return 'bank' if 'banco' in n else 'other'
|
||
# Lists with stable regulator numbers are identities, not fuzzy matches by name.
|
||
broker_names={}
|
||
for row in tables[26][1][1:]:
|
||
for n in [0,2]:
|
||
if row[n]:broker_names[int(row[n])]=row[n+1]
|
||
for t,(section,rows) in tables.items():
|
||
for j,row in enumerate(rows[1:],1):
|
||
e=evidence(t,j,' | '.join(row))
|
||
if t in [12,19,29]:coverage.append({'table':t,'row':j,'status':'aggregate_or_gap_not_an_institution','source_row':e['id']});continue
|
||
if t==26:
|
||
keys=[]
|
||
for n in [0,2]:
|
||
if row[n]:
|
||
k=ensure(row[n+1],'broker','issm-broker-'+row[n].zfill(3));attach(k,e);keys.append(k)
|
||
coverage.append({'table':t,'row':j,'institutions':keys,'source_row':e['id'],'status':'represented'});continue
|
||
if t==27:
|
||
keys=[]
|
||
for n in [0,2]:
|
||
if row[n]:
|
||
k=ensure(row[n+1],'agent','issm-agent-'+row[n].zfill(3));attach(k,e);keys.append(k)
|
||
coverage.append({'table':t,'row':j,'institutions':keys,'source_row':e['id'],'status':'represented'});continue
|
||
if t==25:
|
||
m=re.match(r'Sim\s*\(\s*n[ºo°]?\s*(\d+)',row[5],re.I)
|
||
k=ensure(row[0],'broker','issm-broker-'+m.group(1).zfill(3) if m else 'broker-unconfirmed-'+slug(row[0]));add(t,j,k,e)
|
||
refs[k]['draft']['register_reference']=row[5]
|
||
location(k,'Morada indicada para a corretora',row[1],row[1],e,'contact_point',origin(row[1]+' '+row[3]))
|
||
contacts(k,row[2]+' '+row[4],e,origin(row[3]+' '+row[4]));continue
|
||
if t in [1,3,6,13,14]:
|
||
if t==14 and j in [4,5]:
|
||
names=['Bayport Financial Services','Caixa Mulher'] if j==4 else ['CCOM Maputo Norte','CCOM Maputo Sul','CCOM Matola','Associação dos Camponeses de Macuvulana','Caixa Comunitária de Chókwè','Diocese de Nacala','CCOM Montepuez','CCOM Chiúre','CCOM Namuno','CCOM Ancuabe','CCOM Balama']
|
||
keys=[]
|
||
for name in names:
|
||
k=ensure(name,'microbank' if j==4 else 'savings');attach(k,e);keys.append(k)
|
||
if j==5 and name=='Diocese de Nacala':location(k,'Contacto documentado','Paço Episcopal S. Justino de Jacobis, Nacala-Porto','Nampula',e,'contact_point','historical')
|
||
coverage.append({'table':t,'row':j,'institutions':keys,'source_row':e['id'],'status':'represented_split_explicit_names'});continue
|
||
k=ensure(row[0],typ(row[1]));add(t,j,k,e)
|
||
if t==1:location(k,'Sede',row[2],'Cidade de Maputo',e,'headquarters');contacts(k,row[3],e)
|
||
elif t==3:location(k,'Localização da gestora indicada na fonte',row[2],row[2],e,'contact_point');refs[k]['draft']['register_reference']=row[3]
|
||
elif t==13:location(k,'Local indicado na lista do Banco de Moçambique',row[3],row[2],e,'contact_point','historical')
|
||
elif t==14:
|
||
if j==1:refs[k]['draft'].update({'institution_status':'merged','successor_key':'access-bank'})
|
||
elif j in [2,3]:location(k,'Sede por confirmar',row[2],'Maputo',e,'headquarters','to_confirm')
|
||
continue
|
||
if t in [2,4,5,7,8,9,10,11,15]:
|
||
if t==2:name='Standard Bank';place,label,address,hours=row
|
||
elif t==4:name,label,address,quality=row;place='Cidade de Maputo';hours=''
|
||
elif t in [5,7,9,10]:name={5:'Standard Bank',7:'Socremo',9:'Millennium bim',10:'BCI'}[t];place,label,address=row;hours=''
|
||
elif t==8:name,place,address,quality=row;label='Sede' if 'sede' in norm(name+' '+address) else place;hours=''
|
||
elif t==11:name='BCI';place,label,address,quality=row;hours=''
|
||
else:name,place,label,address,quality=row;hours=''
|
||
k=ensure(name,'microbank' if t in [7,8] and not norm(name).startswith('banco letshego') else 'commercial_bank');add(t,j,k,e)
|
||
state=origin(' | '.join(row))
|
||
if t==4 and 'BCI' in name and j>1:state='to_confirm'
|
||
location(k,label,address,place,e,'headquarters' if 'sede' in norm(label) else 'branch',state,hours);continue
|
||
if t in [16,18,20,28]:
|
||
if t==16:name,address,phone,quality,email=row;place='Cidade de Maputo';tpe=None
|
||
elif t==18:name,address,phone,hours=row;place=address;tpe='exchange_house';quality='Directório — a confirmar';email=''
|
||
elif t==20:place,name,phone,quality=row;address='';email='';tpe='supervisor' if 'ISSM' in name else 'investment_manager' if 'BFS' in name else None
|
||
else:name,address,phone,_=row;place=address;quality='Resumo do documento';email='';tpe='reinsurer' if 'resseguradora' in name else 'insurer'
|
||
k=ensure(name,tpe);add(t,j,k,e);contacts(k,phone+' '+email,e,origin(quality+' '+phone))
|
||
if address:location(k,'Sede ou contacto indicado na fonte',address,place,e,'contact_point',origin(quality+' '+address))
|
||
continue
|
||
if t==17:
|
||
names=row[0].split(', ') if j==7 else [row[0]]
|
||
keys=[]
|
||
for name in names:
|
||
k=ensure(name,'exchange_house');attach(k,e);keys.append(k);location(k,'Morada indicada no documento',row[2],row[1],e,'contact_point','unlocated' if j==7 else 'historical')
|
||
coverage.append({'table':t,'row':j,'institutions':keys,'source_row':e['id'],'status':'represented'});continue
|
||
if t in [21,22,23,24]:
|
||
if t==24:place,name,address,phone=row;tpe='insurer';label=name.split('–')[-1].strip();base_name=name.split('–')[0].strip()
|
||
else:place,name,ramo,address,phone=row;tpe={21:'insurer',22:'reinsurer',23:'microinsurer'}[t];label='Sede';base_name=name
|
||
k=ensure(base_name,tpe);add(t,j,k,e);location(k,label,address,place,e,'branch' if t==24 else 'headquarters');contacts(k,phone,e)
|
||
continue
|
||
raise RuntimeError(f'Unaccounted table {t}, row {j}')
|
||
# Named entries outside tables; retain uncertainty instead of executing prose as instructions.
|
||
for section,text in paragraphs:
|
||
if section.startswith('15.6') and not re.match(r'^15\.6',text):
|
||
for name in ['Chicomo','Renaissance','Pioneer Reinsurance Brokers','Makhaly','Gateway']:
|
||
k=ensure(name,'reinsurance_broker');attach(k,{'id':digest[:16]+':p15.6:'+slug(name),'section':section,'text':text})
|
||
k=ensure('Safeline','microinsurer');attach(k,{'id':digest[:16]+':p15.6:safeline','section':section,'text':text})
|
||
if section.startswith('11.') and 'Executivo Câmbios' in text:
|
||
for name in ['Executivo Câmbios','Sara Moçambique','Al-Meca Câmbios','Acácio Câmbios']:
|
||
k=ensure(name,'exchange_house');refs[k]['draft']['institution_status']='historical';attach(k,{'id':digest[:16]+':p11:'+slug(name),'section':section,'text':text})
|
||
for k,ref in refs.items():
|
||
types=ref['draft']['institution_types']
|
||
if 'bank' in types and any(t in types for t in ['commercial_bank','investment_bank']):types.remove('bank')
|
||
if k=='letshego-microbank-historical':ref['draft']['institution_status']='unknown'
|
||
# Explicit branch/contact facts in prose are not discarded merely because they are outside a table.
|
||
def paragraph_evidence(section,text,tag):return {'id':digest[:16]+':p:'+tag,'section':section,'text':text}
|
||
for section,text in paragraphs:
|
||
if section.startswith('7.') and text.startswith('Além dos Mpoint'):
|
||
e=paragraph_evidence(section,text,'bim-commercial-names');k=ensure('Millennium bim','commercial_bank');attach(k,e)
|
||
names=['Rovuma 191','Rovuma 522','Shoprite Maputo','Baía Mall','Zimpeto Mall','Mica','Marés','Zimpeto Mercado','Shoprite Matola','Parque dos Poetas Prestige','Polana Shopping','Quedas (Centro Comercial Shoprite)','Manica Shopping Center','Matema (Centro Comercial Terrace Africa)','Shoprite Beira','Muahivire']
|
||
for name in names:location(k,name,'Morada por confirmar',name,e,'branch','unlocated')
|
||
elif section.startswith('7.') and text.startswith('Morada encontrada: Polana Shopping'):
|
||
e=paragraph_evidence(section,text,'bim-directory-addresses');k=ensure('Millennium bim','commercial_bank');attach(k,e)
|
||
location(k,'Polana Shopping','Loja 43, piso 0, Polana Shopping & Office Center, Av. 24 de Julho, nº 11','Maputo',e,'branch','reported')
|
||
location(k,'Rovuma — correspondência aos balcões por confirmar','Rua da Sé, nº 114, R/C, Maputo','Maputo',e,'contact_point','to_confirm');contacts(k,'21 312 471',e,'to_confirm')
|
||
elif section.startswith('15.5') and text.startswith('Lista do ISSM, com os 115'):
|
||
e=paragraph_evidence(section,text,'older-broker-names')
|
||
for name in ['Real Risk','Quorus']:
|
||
k=ensure(name,'broker','broker-unconfirmed-'+slug(name));refs[k]['draft']['institution_status']='unknown';attach(k,e)
|
||
k=key('Safeline')
|
||
if k in refs:refs[k]['draft']['institution_status']='unknown';refs[k]['draft']['register_reference']='A fonte menciona Safeline em listas de corretoras e como micro-seguradora; identidade/categoria actual por confirmar.';attach(k,e)
|
||
elif section.startswith('15.6') and text.startswith('O ISSM licencia 39'):
|
||
e=paragraph_evidence(section,text,'agent-and-reinsurance-contacts')
|
||
for k,label,address,state in [('issm-agent-026','Capital Consultoria & Agentes de Seguros','Av. 25 de Setembro, nº 1147, 2A, Maputo','to_confirm'),('issm-agent-027','Carlyle Partners','Rua Eça de Queiroz, nº 135, Coop, Maputo','to_confirm'),('issm-agent-030','Possível morada de homónimo CBM; identidade por confirmar','Rua Fernão Lopes, nº 218, Sommerschield, Maputo','conflict'),('issm-agent-016','Possível morada de homónimo Prestígio; identidade por confirmar','Av. Albert Lithuli, Maputo','conflict')]:
|
||
attach(k,e);location(k,label,address,'Cidade de Maputo',e,'contact_point',state)
|
||
contacts('issm-agent-026','86 442 8633',e,'to_confirm');contacts('issm-agent-001','+258 21 083 820; geral@segurodirecto.co.mz',e,'reported')
|
||
for name,address in [('Renaissance','Av. 25 de Setembro, nº 1230, edifício 33 Andares, R/C, porta 3'),('Gateway','Rua Rofinho de Oliveira, nº 68, 1º andar, Bairro Central')]:
|
||
k=key(name);attach(k,e);location(k,'Contacto de directório, a confirmar',address,'Cidade de Maputo',e,'contact_point','to_confirm')
|
||
elif section.startswith('15.4') and text.startswith('Só encontrei delegações publicadas'):
|
||
e=paragraph_evidence(section,text,'palma-delegations-without-address');k=ensure('Palma Companhia de Seguros','insurer');attach(k,e)
|
||
for name in ['Pemba','Quelimane','Beira','Nampula','Tete']:location(k,'Agência indicada: '+name,'Morada por confirmar',name,e,'branch','unlocated')
|
||
# Whole source notes remain documentary evidence, never operational commands.
|
||
notes=[{'section':s,'text':t} for s,t in paragraphs if not re.match(r'^\d+\.',t)]
|
||
result={'format':'investmoz-funding-references-v1','source':{'name':args.source.name,'sha256':digest,'edition':'Novembro de 2026'},'references':refs,'source_coverage':coverage,'source_notes':notes}
|
||
args.output.parent.mkdir(parents=True,exist_ok=True)
|
||
args.output.write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||
print(json.dumps({'institutions':len(refs),'locations':sum(len(x['draft']['locations']) for x in refs.values()),'contacts':sum(len(x['draft']['contacts']) for x in refs.values()),'table_rows_accounted':len(coverage),'categories':sorted({c for x in refs.values() for c in x['draft']['institution_categories']}),'source_sha256':digest}))
|