554 lines
20 KiB
JavaScript
554 lines
20 KiB
JavaScript
// 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(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/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;
|
||
});
|