// 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(/]*?)?(?:\/>|>[\s\S]*?<\/w:p>)/g)].map( (m, i) => ({ p: i + 1, text: decode( [...m[0].matchAll(/]*)?>([\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], ) || (/