276 lines
10 KiB
JavaScript
276 lines
10 KiB
JavaScript
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,
|
||
}),
|
||
);
|