Files
Investidor-V2/tools/investmoz-v2-territory-report.cjs
T

217 lines
9.1 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const { reconcile } = require('./investmoz-v2-source-reconciliation.cjs');
const root = path.resolve(__dirname, '..');
const audit = JSON.parse(
fs.readFileSync(
path.join(
root,
'storage/app/private/investmoz-v2/territory-audit.json',
),
'utf8',
),
);
if (audit.identity.read_only !== 'on')
throw new Error('Read-only evidence required');
const geo = new Map(audit.geography.map((g) => [g.id, g]));
const clean = (value) =>
String(value ?? '')
.replaceAll('|', '\\|')
.replaceAll('\n', ' ');
const lineage = (id) => {
const visited = new Set();
const nodes = [];
const issues = [];
while (id) {
if (visited.has(id)) {
issues.push('ciclo');
break;
}
visited.add(id);
const g = geo.get(id);
if (!g) {
issues.push('nó/antepassado ausente');
break;
}
nodes.unshift(g);
id = g.parent_id;
}
const ranks = [
'country',
'province',
'district',
'admin_post',
'locality',
'site',
];
for (let i = 1; i < nodes.length; i++) {
if (
nodes[i].location_type !== 'site' &&
ranks.indexOf(nodes[i].location_type) !==
ranks.indexOf(nodes[i - 1].location_type) + 1
)
issues.push('hierarquia incompatível');
}
return { names: nodes.map((g) => g.name).join(' → '), issues };
};
const lines = [
'# Qualidade territorial e relações — entrega V2',
'',
`Leitura protegida em ${audit.captured_at}, base ativa ${audit.identity.database}:${audit.identity.port}, transação READ ONLY terminada por ROLLBACK. Nenhum dado ativo alterado. Evidência completa privada: \`territory-audit.json\`.`,
'',
'## Resultados por registo e propostas de dados',
'',
'Cada linha identifica o estado cadastrado, a fonte consultada, motivo e impacto da proposta. Documentos oficiais são a única fonte de validação oficial. Ausência de campos de fonte/data é uma lacuna de rastreabilidade, não invalidade ou revogação. Estados e aceitações existentes são preservados na [matriz de reconciliação](reconciliacao-fontes-territoriais.md). Sem associação explícita não se cria uma relação por nome semelhante, cluster ou proximidade.',
'',
'| Registo | Relações/localizações cadastradas | Fonte consultada | Proposta, motivo e impacto |',
'| --- | --- | --- | --- |',
];
const result = [];
for (const item of audit.records) {
const r = item.record;
const reconciled = reconcile(item, audit);
const metadata = JSON.parse(r.metadata || '{}');
const own = (item.relations.own || []).map((l) => ({
id: l.geo_location_id,
role: l.role || l.scope_type || 'declarada',
source:
metadata.documented_location_sources?.[
l.geo_location_id + ':' + l.role
]?.source_reference ||
l.source_reference ||
l.source_system ||
null,
}));
if (
r.primary_territory_id &&
!own.some((l) => l.id === r.primary_territory_id)
)
own.unshift({
id: r.primary_territory_id,
role: 'principal cadastrada',
source: r.source_system || null,
});
for (const c of metadata.documented_coverage || [])
own.push({
id: c.id,
role: 'abrangência',
source: c.source_reference || null,
});
const locations = own.map((l) => ({ ...l, ...lineage(l.id) }));
const dossiers = audit.dossiers.filter(
(d) => d.project_id === r.id || d.cluster_id === r.id,
);
const source =
[
...new Set(
[
r.source_system,
...dossiers.map((d) => d.source),
...locations.map((l) => l.source),
].filter(Boolean),
),
].join('; ') ||
'Cadastro da oportunidade; fonte da relação e data ausentes';
const proposed = [];
if (item.kind === 'opportunities')
proposed.push(
'Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente',
);
if (!own.length)
proposed.push(
'Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto',
);
if (locations.some((l) => l.issues.length))
proposed.push(
'Rever hierarquia indicada na evidência; não mudar parent_id automaticamente',
);
if (own.length > 1)
proposed.push(
'Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide',
);
if (item.kind === 'economic_clusters')
proposed.push(
'Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro',
);
if (item.kind === 'investment_zones')
proposed.push(
'Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade',
);
if (!proposed.length)
proposed.push(
'Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro',
);
proposed.push(
'Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável',
);
const label = r.code || r.reference || String(r.id);
const relations =
locations
.map(
(l) =>
`${l.role}: ${l.names || l.id}${l.issues.length ? ' [' + l.issues.join(', ') + ']' : ''}`,
)
.join('; ') || 'Sem localização estruturada';
const counts = `projetos: ${(item.relations.projects || []).length}; clusters: ${(item.relations.clusters || []).length}; membros: ${(item.relations.members || []).length}; ligações de zona: ${(item.relations.links || []).length}`;
lines.push(
`| ${clean(label)} | ${clean(relations + '; ' + counts)} | ${clean(source + '; cadastro: ' + (reconciled.existing_record_validation ?? 'sem campo/estado registado') + '; fonte legal: ' + (reconciled.existing_legal_validation ?? 'sem ligação no cadastro'))} | ${clean(proposed.join('; '))} |`,
);
result.push({
kind: item.kind,
id: r.id,
reference: label,
locations,
proposals: proposed,
source_reconciliation: reconciled,
});
}
const hierarchy = audit.geography
.map((g) => ({ id: g.id, ...lineage(g.id) }))
.filter((g) => g.issues.length);
lines.push(
'',
'## Hierarquia e cartografia',
'',
`${audit.geography.length} nós administrativos/locais percorridos; ${hierarchy.length} caminhos com inconsistência estrutural identificada por esta verificação. Ausência de inconsistência cadastral não certifica fronteiras oficiais. ${audit.geometries.length} geometrias ativas cadastradas: nenhum ponto, polígono ou centroide gerado.`,
'',
'## Correções de código e QA',
'',
'- Ficha apresenta implantação declarada separada dos locais/impacto dos projetos e abrangência dos clusters; fonte/data ausentes explícitas.',
'- Filtros distinguem implantação declarada de apenas contexto associado; localizações sem coordenadas continuam pesquisáveis.',
'- Mapa mantém vários locais do mesmo investimento e seleção independente; áreas cadastradas são apresentadas com proveniência, sem homologar limites.',
'- Associação existente continua a permitir navegação oportunidade → dossier → oportunidade e território → pesquisa. Permissões e relações revogadas preservadas.',
'',
'Ver resultados de QA no backlog e na matriz de reconciliação. Propostas desta tabela não são alterações aplicadas nem plano de escrita aprovado; não revogam validações documentais existentes.',
'',
'## Pendências independentes',
'',
'Diferença de cobertura do inventário documental, validação jurídica/documental das fontes, ClamD real e primeiro pedido real no Balcão permanecem separados. Nenhum teste territorial conclui essas pendências. Aplicação futura de dados exige backup/restauro confirmado e ação autenticada existente.',
);
fs.writeFileSync(
path.join(root, 'docs/investmoz/v2/qualidade-territorial.md'),
lines.join('\n') + '\n',
);
fs.writeFileSync(
path.join(
root,
'storage/app/private/investmoz-v2/territory-proposals.json',
),
JSON.stringify(
{
captured_at: audit.captured_at,
applied: false,
records: result,
hierarchy,
},
null,
2,
),
);
console.log(
JSON.stringify({
records: result.length,
hierarchy_issues: hierarchy.length,
geometry_count: audit.geometries.length,
}),
);