Files
Investidor-V2/tools/portal-audit-reconcile.cjs
T
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

554 lines
20 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.
// Source inventory and conservative initial matrix. Does not connect to a database.
const fs = require('node:fs'),
path = require('node:path'),
crypto = require('node:crypto');
const JSZip = require('jszip');
const root = path.resolve(__dirname, '..');
const priv = path.join(root, 'storage/app/private/investmoz-v2/portal-audit');
const out = path.join(root, 'docs/investmoz/v2/auditoria-portal');
fs.mkdirSync(priv, { recursive: true });
fs.mkdirSync(out, { recursive: true });
const hash = (b) => crypto.createHash('sha256').update(b).digest('hex');
const read = (f) => {
const b = fs.readFileSync(path.join(root, f));
return b
.toString(b[0] === 255 && b[1] === 254 ? 'utf16le' : 'utf8')
.replace(/^\uFEFF/, '');
};
function csv(text) {
const rows = [];
let row = [],
cell = '',
quote = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === '"') {
if (quote && text[i + 1] === '"') {
cell += '"';
i++;
} else quote = !quote;
} else if (c === ',' && !quote) {
row.push(cell);
cell = '';
} else if (c === '\n' && !quote) {
row.push(cell.replace(/\r$/, ''));
if (row.some(Boolean)) rows.push(row);
row = [];
cell = '';
} else cell += c;
}
if (cell || row.length) {
row.push(cell);
rows.push(row);
}
return rows
.slice(1)
.map((r) => Object.fromEntries(rows[0].map((h, i) => [h, r[i] || ''])));
}
function walk(dir) {
let all = [];
if (!fs.existsSync(dir)) return all;
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const f = path.join(dir, e.name);
if (e.isDirectory()) all.push(...walk(f));
else if (e.isFile()) all.push(f);
}
return all;
}
const decode = (s) =>
s
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'");
function paragraphs(xml) {
return [...xml.matchAll(/<w:p(?:\s[^>]*?)?(?:\/>|>[\s\S]*?<\/w:p>)/g)].map(
(m, i) => ({
p: i + 1,
text: decode(
[...m[0].matchAll(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g)]
.map((x) => x[1])
.join(''),
),
}),
);
}
const routes = JSON.parse(
read('storage/app/private/investmoz-v2/portal-audit-routes.json'),
);
const domains = [
[
'oportunidades',
'oportunidad|investment_project|imobili|duat|fundiári|franquia|RF-AGR|RF-OPP',
'OpportunityManagement|OpportunityResource|PublicBusiness|LandAssistance',
[
'app/Services/OpportunityManagement.php',
'app/Services/OpportunityResources.php',
'resources/js/pages/management/opportunity-draft.tsx',
],
],
[
'cartografia/relações',
'territ|cartograf|geometri|coordenad|cluster|corredor|RF-CLU|RF-COR|RF-ZON',
'Territor|InvestmentStructure|InvestmentDiscovery|Zone|Cluster',
[
'resources/js/components/investment-territory.tsx',
'app/Http/Controllers/InvestmentTerritoryController.php',
],
],
[
'Balcão/orientação',
'balcão|balcao|dossier|orienta|jornada|encaminh|institucion|recomend',
'VirtualDesk|InvestorGuidance',
[
'app/Services/VirtualDesk.php',
'app/Services/Ai/InvestorGuidance.php',
],
],
[
'CRM/solicitações',
'crm|lead|solicita|manifesta|contacto|responsável|RF-CRM',
'Crm|InvestmentSubmission|PortalServices|InvestorWorkspace',
[
'app/Http/Controllers/CrmController.php',
'app/Services/DeclaredOriginStatistics.php',
],
],
[
'simuladores',
'simula|fisc|tribut|fluxo de caixa|cálculo|calculador',
'Simulation',
[
'app/Services/SimulationTaxParameterResolver.php',
'resources/js/components/simulation-form.tsx',
],
],
[
'biblioteca',
'biblioteca|document|acervo|anexo|scan|malware|formulário',
'PortalServices|Document|Legal',
[
'app/Http/Controllers/PortalServicesController.php',
'app/Jobs/ScanDocumentForMalware.php',
],
],
[
'educação',
'educa|competência|capacitação|RF-EDU|RNF-EDU',
'Education',
['app/Http/Controllers/EducationController.php'],
],
[
'Bolsa/parcerias',
'parcer|bolsa|candidatur|acordo',
'Partnership',
['app/Services/PartnershipExchange.php'],
],
[
'negócios',
'negócio|empresa|RF-MNE|RF-MKT',
'Business',
['app/Http/Controllers/ExistingBusinessController.php'],
],
[
'dashboard/perfis',
'dashboard|menu|perfil|país|origem|estatístic|residên|RF-INV',
'Dashboard|InvestmentWorkspace',
[
'app/Http/Controllers/DashboardController.php',
'app/Services/CountryCatalogue.php',
],
],
['governação/outros', '.*', 'Management', ['AGENTS.md']],
];
const rows = [];
const keys = new Map();
function add(id, request, source, criterion = '', variant = '') {
request = request.trim();
if (!request) return;
const key = id + '\0' + request;
if (keys.has(key)) {
const existing = keys.get(key);
if (!existing.fontes.includes(source)) existing.fontes.push(source);
return;
}
const d = domains.find((d) =>
new RegExp(d[1], 'i').test(id + ' ' + request),
);
const found = routes
.filter((r) => new RegExp(d[2], 'i').test(r.action))
.map((r) => '/' + r.uri)
.filter((v, i, a) => a.indexOf(v) === i);
const code = d[3].filter((f) => fs.existsSync(path.join(root, f)));
const item = {
id,
pedido: request,
fontes: [source],
modulo: d[0],
rotas: found.slice(0, 12),
implementacao: code.length
? code.join('; ') +
' — componentes do domínio encontrados; correspondência integral deste requisito não presumida'
: 'Correspondência concreta ainda não localizada',
ativo: 'Consultar estado-ativo.json; existência de tabela não certifica este requisito',
evidencia:
'Sem ensaio individual nesta matriz inicial; cruzar ensaios-rotas.json e suites por domínio',
estado: 'parcial',
lacuna: 'Aceitação integral por requisito ainda não demonstrada; verificar campos, ações e recuperação',
dependencia:
'Revisão individual e ensaio QA; nenhuma nova validação de credibilidade oficial',
aceitacao: criterion || request,
prioridade: 'P2',
variantes: variant,
};
rows.push(item);
keys.set(key, item);
}
(async () => {
const previous = JSON.parse(
read('storage/app/private/investmoz-v2/legal-journey-inventory.json'),
);
const sourceRoot =
'C:/Mac/Home/Documents/TEMPO/Portal Tempo/Servicos Tempo';
const sourceRoots = [
...new Set([
sourceRoot,
...previous.roots,
...[
'portal-documents',
'ai-documents',
'document-import-staging',
'geo-ine-source-20260928',
'opportunity-resources',
].map((directory) =>
path.join(root, 'storage/app/private', directory),
),
]),
];
const external = [...new Set(sourceRoots.flatMap(walk))],
local = walk(path.join(root, 'docs/investmoz')).filter(
(f) => !f.includes(path.sep + 'auditoria-portal' + path.sep),
);
const inventory = [];
const masters = [];
for (const f of [...external, ...local]) {
const bytes = fs.readFileSync(f);
const entry = {
path: f,
bytes: bytes.length,
sha256: hash(bytes),
attachments: [],
consultation: external.includes(f)
? 'Acervo inventariado; consulta/publicação individual não inferida'
: 'Documentação do repositório',
rule_use: 'Nenhum cálculo ativado pela auditoria',
};
if (/\.(docx|xlsx|pptx|zip)$/i.test(f)) {
try {
const z = await JSZip.loadAsync(bytes);
for (const [name, e] of Object.entries(z.files)) {
if (
!e.dir &&
/^(word\/embeddings\/|word\/media\/|xl\/embeddings\/|ppt\/embeddings\/)/.test(
name,
)
) {
const b = await e.async('nodebuffer');
entry.attachments.push({
path: name,
bytes: b.length,
sha256: hash(b),
});
}
}
if (
/\.docx$/i.test(f) &&
/upgrade do portal|melhorar e transformar|Reformulacao do portal|Prompt master|Especificação do Formulário de Cadastro de Oportunidades|Competências e Oportunidades Educativas|prompt de gestao de educação/i.test(
path.basename(f),
)
) {
const x = z.file('word/document.xml');
if (x) {
const ps = paragraphs(await x.async('string'));
masters.push({
source: f,
hash: entry.sha256,
paragraphs: ps,
});
}
}
} catch (e) {
entry.read_error = e.message;
}
}
inventory.push(entry);
if (/\.txt$/i.test(f) && external.includes(f)) {
const text = bytes
.toString(
bytes[0] === 255 && bytes[1] === 254 ? 'utf16le' : 'utf8',
)
.replace(/^\uFEFF/, '');
masters.push({
source: f,
hash: entry.sha256,
paragraphs: text
.split(/\r?\n/)
.map((text, i) => ({ p: i + 1, text })),
line_numbered: true,
});
}
}
// All available source files inventoried before selecting requirements. Preserve earlier inaccessible inventory too.
fs.writeFileSync(
path.join(priv, 'inventario-fontes.json'),
JSON.stringify(
{
captured_at: new Date().toISOString(),
source_roots: sourceRoots,
source_root_accessible: fs.existsSync(sourceRoot),
files: inventory,
previous_inventory: previous.entries.map((e) => ({
path: e.path,
sha256: e.sha256,
})),
masters: masters.map((m) => ({
source: m.source,
sha256: m.hash,
paragraphs: m.paragraphs.length,
line_numbered: m.line_numbered || false,
})),
},
null,
2,
),
);
for (const r of csv(read('docs/investmoz/requirements-audit.csv')))
add(
r.ID,
r.Requisito,
'upgrade v4 SHA f0b9815…; p. ' +
r.Paragrafo +
'; requirements-audit.csv',
r.Criterio,
'Relatório 25/09 conservado; estado histórico não promove conclusão',
);
for (const r of csv(read('docs/investmoz/education/requirements.csv')))
add(
r.id,
r.descricao,
'education/requirements.csv; ' + r.artefacto,
r.criterio_aceitacao,
'Proposta e revisões em education/implementation-status.md',
);
for (const r of csv(read('docs/investmoz/education/nonfunctional.csv')))
add(
r.id,
r.area + ' — ' + r.metrica,
'education/nonfunctional.csv',
r.limiar_proposto,
'Limiar proposto não é resultado medido',
);
for (const r of csv(
read('docs/investmoz/existing-business/traceability.csv'),
))
add(
r.rf_local,
r.objecto + '; ' + r.regra_juridica,
'existing-business/traceability.csv',
r.teste_planeado,
'Escopo local RF-MNE; conservar homónimos do mestre',
);
for (const line of read('docs/investmoz/v2/backlog-mestre.md').split(
'\n',
)) {
const cells = line.split('|').map((s) => s.trim());
if (/^V2-\d{2}$/.test(cells[1] || ''))
add(
cells[1],
cells[3],
`backlog-mestre.md; ${cells[2]}`,
cells[4],
'Estado da primeira entrega; confirmações posteriores permanecem separadas',
);
}
let obligationCount = 0;
const obligation =
/\b(deve[mr]?|obrigatóri[oa]s?|exigir|permitir|impedir|guardar|preservar|validar|registar|disponibilizar|reutilizar|implementar|assegurar|garantir|não inferir|não inventar|recuperar|publicar|rejeitar|seleccionar|selecionar)\b/i;
for (const m of masters) {
let counter = 0;
const seen = new Map();
for (const p of m.paragraphs) {
const text = p.text.trim();
if (
text.length < 24 ||
text.length > 4000 ||
/^(CREATE |ALTER |INSERT |SELECT |DROP |--|\{|\}|\[|\]|"\w+"\s*:)/i.test(
text,
)
)
continue;
if (/oportunidades/i.test(m.source) && p.p > 2107 && p.p < 14682)
continue;
if (!obligation.test(text) && !/^(REQ\d+|DT\d+|RF[-–])/i.test(text))
continue;
const normal = text.replace(/\s+/g, ' '),
label = /oportunidades/i.test(m.source)
? 'OP'
: /melhorar e transformar/i.test(m.source)
? 'M2'
: 'MASTER';
const identifier =
seen.get(normal) ||
`${label}-${m.hash.slice(0, 8)}-${String(++counter).padStart(4, '0')}`;
seen.set(normal, identifier);
add(
identifier,
normal,
`${path.relative(sourceRoot, m.source)}; SHA ${m.hash}; ${m.line_numbered ? 'linha' : 'w:p'} ${p.p}`,
'',
'Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas',
);
obligationCount++;
}
}
const fields = JSON.parse(
read('storage/app/private/investmoz-v2/opportunity-schema-index.json'),
);
fs.writeFileSync(
path.join(priv, 'schema-spec-index.json'),
JSON.stringify(fields, null, 2),
);
for (const definition of fields) {
for (const field of (definition.fields || '')
.split(',')
.filter(Boolean)) {
const required = (definition.required || '')
.split(',')
.includes(field);
add(
`OP-FIELD-${definition.name}-${field}`,
`Campo ${definition.name}.${field}; ${required ? 'obrigatório na validação final quando aplicável' : 'opcional/condicional; conferir contrato'}; rascunho incompleto permitido`,
'Especificação de oportunidades SHA 9f173ed3…; §26; opportunity-schema-index.json',
`Persistir, recuperar e validar ${definition.name}.${field} segundo o schema da categoria; nunca bloquear rascunho apenas por exigência final`,
'Campo do contrato técnico; IDs bigint atuais prevalecem sobre exemplos UUID',
);
}
}
const consumerRows = [];
for (const file of walk(path.join(root, 'resources/js')).filter((f) =>
/\.(tsx|ts)$/.test(f),
)) {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (
/<(ProvinceSelect|TerritoryFields|TerritoryLevel|CountrySelect)|<.*Map\b|name=["'](?:province|district|locality|territory)/.test(
lines[i],
) ||
(/<select/.test(lines[i]) &&
/territor|location|provinc|district|locality/.test(
lines.slice(Math.max(0, i - 5), i + 12).join('\n'),
))
)
consumerRows.push({
file: path.relative(root, file).replace(/\\/g, '/'),
line: i + 1,
component: lines[i].trim(),
coverage:
'Inventariado; teste individual de UI ainda pendente',
});
}
}
fs.writeFileSync(
path.join(priv, 'consumidores-campos.json'),
JSON.stringify(consumerRows, null, 2),
);
for (const [i, c] of consumerRows.entries())
add(
`AUD-FIELD-${String(i + 1).padStart(3, '0')}`,
`Pesquisar, selecionar, guardar e recuperar no consumidor ${c.file}:${c.line}; ${c.component}`,
'Pedido direto 09/10/2026, §1 cartografia em todos os campos; inventário de consumidores',
`Demonstração individual em 1440 e 390 px; selecção respeita pai e ID guardado; ausência de geometria mantém indeterminação`,
'Consumidores independentes; componente comum não demonstra todos os usos',
);
const headers = [
'ID',
'Pedido/trecho',
'Fonte/ocorrências',
'Módulo',
'Rotas',
'Implementação encontrada',
'Disponibilidade ativa',
'Evidência funcional',
'Estado',
'Lacuna',
'Dependência',
'Critério de aceitação',
'Prioridade',
'Versões divergentes',
];
function save() {
fs.writeFileSync(
path.join(priv, 'matriz.json'),
JSON.stringify(rows, null, 2),
);
fs.writeFileSync(
path.join(out, 'matriz-requisitos.csv'),
'\uFEFF' +
[
headers,
...rows.map((r) => [
r.id,
r.pedido,
r.fontes.join(' | '),
r.modulo,
r.rotas.join('; '),
r.implementacao,
r.ativo,
r.evidencia,
r.estado,
r.lacuna,
r.dependencia,
r.aceitacao,
r.prioridade,
r.variantes,
]),
]
.map((row) =>
row
.map(
(v) =>
'"' + String(v).replace(/"/g, '""') + '"',
)
.join(','),
)
.join('\n') +
'\n',
);
}
save();
const summary = {
files: inventory.length,
external_files: external.length,
attachments: inventory.reduce((n, e) => n + e.attachments.length, 0),
masters: masters.map((m) => ({
file: path.basename(m.source),
sha256: m.hash,
paragraphs: m.paragraphs.length,
})),
requirements: rows.length,
obligation_occurrences: obligationCount,
route_count: routes.length,
status: 'Matriz inicial conservadora; não é certificação de aceitação',
};
fs.writeFileSync(
path.join(priv, 'inventario-resumo.json'),
JSON.stringify(summary, null, 2),
);
console.log(JSON.stringify(summary));
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});