Files
Investidor-V2/tools/investmoz-v2-prepare-territorial-plan.cjs
T

276 lines
10 KiB
JavaScript
Raw 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.
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const root = path.resolve(__dirname, '..');
const dir = path.join(root, 'storage/app/private/investmoz-v2');
const audit = JSON.parse(
fs.readFileSync(path.join(dir, 'territory-audit.json'), 'utf8'),
);
const reconciliation = JSON.parse(
fs.readFileSync(
path.join(dir, 'territory-source-reconciliation.json'),
'utf8',
),
);
if (
audit.identity.read_only !== 'on' ||
reconciliation.captured_at !== audit.captured_at
)
throw new Error('Matching read-only source reconciliation required');
const plan = {
schema: 1,
prepared_at: new Date().toISOString(),
target: {
database: audit.identity.database,
port: Number(audit.identity.port),
},
code_hash: '',
rows: [],
updates: [],
guards: [],
pending: [],
new_relationships: [],
};
const guardIds = new Set();
const guard = (table, record, fields) => {
const key = table + ':' + record.id;
if (guardIds.has(key)) return;
guardIds.add(key);
plan.guards.push({
table,
id: record.id,
expected: Object.fromEntries(
fields
.filter((field) => field in record)
.map((field) => [field, record[field]]),
),
});
};
const publicDossiers = audit.dossiers.filter(
(d) => d.publication_status === 'published',
);
for (const entity of audit.records) {
const record = entity.record;
const reference = record.code || record.reference || String(record.id);
const source = reconciliation.records.find(
(r) => r.kind === entity.kind && r.id === entity.id,
);
guard(entity.kind, record, [
'version_no',
'territory_version',
'metadata',
'source_evidence',
'validation_status',
'publication_status',
'status',
'primary_territory_id',
'deleted_at',
]);
const dossiers = audit.dossiers.filter(
(d) => d.project_id === record.id || d.cluster_id === record.id,
);
for (const dossier of dossiers)
guard('investment_dossiers', dossier, [
'version_no',
'source',
'content',
'publication_status',
]);
const visible =
entity.kind === 'opportunities'
? record.status === 'published'
: entity.kind === 'investment_zones'
? record.publication_status === 'published'
: dossiers.some((d) => d.publication_status === 'published');
const metadata = JSON.parse(record.metadata || '{}');
const legal = source.legal_source;
if (legal) guard('investment_legal_sources', legal, Object.keys(legal));
const evidence =
source.official_references[0] || source.non_official_references[0];
const row = (association, role, props = {}) => {
plan.rows.push({
key: `${entity.kind}:${entity.id}:${association}:${role}`,
kind: entity.kind,
record_id: entity.id,
reference,
association,
role,
public: visible,
source_reference: legal
? `${legal.diploma} — ${legal.url}`
: evidence
? `${evidence.title} — ${evidence.url}`
: dossiers[0]?.source || null,
source_quote: null,
supports:
evidence?.supports ||
'Origem oficial aceite pelo responsável; ligação ao exemplar a recuperar onde ausente.',
existing_validation: record.validation_status ?? null,
official_source_accepted: Boolean(
legal || source.official_references.length,
),
traceability: source.traceability,
effect: 'Ficha mostra fundamento e pendência específica; identidade, geometria, filtros e validações preservados.',
...props,
});
};
row('registo', 'proveniência');
const locations = entity.relations.own || [];
for (const location of locations) {
if (location.id)
guard(
entity.kind === 'projects'
? 'project_locations'
: 'investment_zone_locations',
location,
Object.keys(location),
);
const saved =
metadata.documented_location_sources?.[
location.geo_location_id + ':' + location.role
];
row(
location.geo_location_id,
location.role || location.scope_type || 'implantação',
{
public_location_id: location.geo_location_id,
source_reference:
saved?.source_reference ||
location.source_reference ||
legal?.url ||
evidence?.url ||
null,
source_quote: saved?.source_quote || null,
supports:
saved?.reason ||
evidence?.supports ||
'Ligação ao documento utilizado por recuperar; dado preservado.',
dossier_id: saved?.dossier_id || null,
},
);
}
if (
record.primary_territory_id &&
!locations.some(
(l) => l.geo_location_id === record.primary_territory_id,
)
)
row(
record.primary_territory_id,
entity.kind === 'opportunities'
? 'implantação cadastrada'
: 'território principal',
{
public_location_id: record.primary_territory_id,
supports:
'Associação já existente; nomes administrativos provenientes do workbook cartografia-moz. Não se atribui automaticamente o PDF INE a este vínculo.',
},
);
for (const coverage of metadata.documented_coverage || [])
row(coverage.id, 'abrangência', {
source_reference: coverage.source_reference || null,
source_quote: coverage.source_quote || null,
public_location_id: coverage.id,
public: visible && coverage.data_classification === 'public',
});
if (entity.kind === 'economic_clusters') {
const dossier = publicDossiers.find((d) => d.cluster_id === record.id);
const content = JSON.parse(dossier?.content || '{}');
for (const member of entity.relations.members || []) {
const project = audit.records.find(
(r) => r.kind === 'projects' && r.id === member.project_id,
);
const quote = content.anchors || '';
const explicit =
project &&
new RegExp(
'(^|[^A-Za-z0-9])' +
project.record.code +
'([^A-Za-z0-9]|$)',
).test(quote);
guard('cluster_projects', member, Object.keys(member));
row(member.project_id, 'contexto — membro ' + member.role, {
association_id: member.id,
dossier_id: dossier?.id || null,
source_reference:
member.source_reference ||
(explicit ? dossier.source : null),
source_quote: explicit ? quote : null,
existing_validation: member.validation_status,
official_source_accepted: false,
traceability: explicit
? 'citação recuperada; ponte ao exemplar oficial por recuperar'
: 'rastreabilidade por recuperar',
effect: 'Citação da relação cluster–projeto passa a ser visível; não muda validação nem cria implantação da oportunidade.',
});
if (
explicit &&
!member.source_reference &&
!member.evidence_reference
)
plan.updates.push({
table: 'cluster_projects',
id: member.id,
reference: `${reference} → ${project.record.code}`,
current: {
source_reference: null,
evidence_reference: null,
},
values: {
source_reference: dossier.source,
evidence_reference: dossier.id,
},
source_quote: quote,
effect: 'Relação existente reutilizada; apenas referências preenchidas.',
});
}
}
if (source.traceability !== 'official_reference_identified')
plan.pending.push({
reference,
reason: 'Ponte ao exemplar oficial não identificada no cadastro revisto; recuperar sem desvalidar.',
});
if (
entity.kind === 'opportunities' &&
!(entity.relations.projects || []).length &&
!(entity.relations.clusters || []).length
)
plan.pending.push({
reference,
reason: 'Sem correspondência documental explícita a uma âncora/cluster nas fontes já reconciliadas; não criar ligação por setor ou proximidade.',
});
}
const files = [
'app/Services/InvestmentTerritorialTraceability.php',
'app/Http/Controllers/InvestmentRelationshipRepairController.php',
'resources/js/components/territorial-traceability-plan.tsx',
'tools/investmoz-v2-prepare-territorial-plan.cjs',
];
plan.code_hash = crypto
.createHash('sha256')
.update(
files
.map((file) =>
crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(root, file)))
.digest('hex'),
)
.join(''),
)
.digest('hex');
fs.writeFileSync(
path.join(dir, 'territory-activation-plan.json'),
JSON.stringify(plan, null, 2),
);
console.log(
JSON.stringify({
records: audit.records.length,
associations: plan.rows.length,
updates: plan.updates.length,
pending: plan.pending.length,
new_relationships: 0,
target: plan.target,
}),
);