217 lines
9.1 KiB
JavaScript
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,
|
|
}),
|
|
);
|