Liga acervo documental e guias a jornada do investidor

This commit is contained in:
2026-10-08 18:54:24 +02:00
parent 51c7fb92b1
commit 30acdc9934
18 changed files with 14024 additions and 1 deletions
+91
View File
@@ -0,0 +1,91 @@
const fs = require('fs');
const d = 'storage/app/private/investmoz-v2/';
const read = (f) =>
JSON.parse(fs.readFileSync(d + f, 'utf8').replace(/^\uFEFF/, ''));
const norm = (s) =>
s
.replace(/[\u200b\ufeff]/g, '')
.replace(/\s+/g, ' ')
.trim()
.replace(
'2. Visão, objectivos e princípios',
'2. Visão, objetivos e princípios',
);
const clean = (a) =>
a
.filter((p) => !/(ndice|toc)/i.test(p.style) && norm(p.text))
.map((p) => ({ ...p, text: norm(p.text) }));
const a = clean(read('master-paragraphs.json')),
b = clean(read('master-paragraphs-reopened.json'));
const pairs = [];
function row(x0, x1, y0, y1, reverse) {
const size = y1 - y0;
let prev = new Uint32Array(size + 1),
curr = new Uint32Array(size + 1);
for (let i = 0; i < x1 - x0; i++) {
curr[0] = 0;
const text = a[reverse ? x1 - 1 - i : x0 + i].text;
for (let j = 1; j <= size; j++)
curr[j] =
text === b[reverse ? y1 - j : y0 + j - 1].text
? prev[j - 1] + 1
: Math.max(prev[j], curr[j - 1]);
[prev, curr] = [curr, prev];
}
return prev;
}
function align(x0, x1, y0, y1) {
while (x0 < x1 && y0 < y1 && a[x0].text === b[y0].text)
pairs.push([x0++, y0++]);
const tail = [];
while (x0 < x1 && y0 < y1 && a[x1 - 1].text === b[y1 - 1].text)
tail.push([--x1, --y1]);
if (x0 < x1 && y0 < y1) {
if (x1 - x0 === 1) {
for (let j = y0; j < y1; j++)
if (a[x0].text === b[j].text) {
pairs.push([x0, j]);
break;
}
} else {
let mid = (x0 + x1) >> 1;
let f = row(x0, mid, y0, y1, false),
r = row(mid, x1, y0, y1, true),
split = 0,
best = -1;
for (let j = 0; j <= y1 - y0; j++)
if (f[j] + r[y1 - y0 - j] > best) {
best = f[j] + r[y1 - y0 - j];
split = j;
}
align(x0, mid, y0, y0 + split);
align(mid, x1, y0 + split, y1);
}
}
pairs.push(...tail.reverse());
}
align(0, a.length, 0, b.length);
const ma = new Set(pairs.map((p) => p[0])),
mb = new Set(pairs.map((p) => p[1]));
const removed = a.filter((p, i) => !ma.has(i)),
added = b.filter((p, i) => !mb.has(i));
const result = {
method: 'LCS paragraph alignment; exclude TOC and empty paragraphs; normalize heading orthography only',
matched: pairs.length,
removed,
added,
mapping: pairs.map(([i, j]) => ({ old: a[i].p, current: b[j].p })),
};
fs.writeFileSync(
d + 'master-aligned-changes.json',
JSON.stringify(result, null, 2),
);
console.log(
JSON.stringify({
matched: pairs.length,
removed: removed.length,
added: added.length,
addedTexts: added.slice(0, 8),
removedFirst: removed.slice(0, 4),
}),
);
+150
View File
@@ -0,0 +1,150 @@
const fs = require('node:fs');
const dir = 'storage/app/private/investmoz-v2/';
const p = JSON.parse(fs.readFileSync(dir + 'master-paragraphs.json', 'utf8'));
const diff = JSON.parse(
fs.readFileSync('docs/investmoz/v2/repeticoes-reconciliacao.json', 'utf8'),
).repeated.filter((x) => x.comparison === 'historical_content_differs');
const proposals = [
[
'Índices e paginação',
'Os intervalos abrangem sumários extensos; a diferença não equivale a regra funcional.',
'Preservar ambos os índices e usar títulos/identificadores de parágrafo, sem páginas impressas como chave.',
],
[
'Linhas vazias',
'Não altera o fluxo BM/AT.',
'Conservar o texto comum; interoperabilidade permanece fora da fase.',
],
[
'Linhas vazias',
'Não altera benefícios nem ativa cálculos.',
'Unir o conteúdo comum, conservando referências de ambas as ocorrências.',
],
[
'Fluxo deslocado e texto introdutório',
'A primeira ocorrência termina com a introdução de outro documento; a segunda contém a tabela de 14 etapas, também repetida após §13.',
'Separar introdução e tabela; reutilizar a tabela como guia de preparação, sem conceder benefícios nem executar atos externos.',
],
[
'Comprimento dos espaços de preenchimento',
'Não altera nomes dos campos.',
'Reutilizar os mesmos campos; preservar os exemplares originais, sem escolher por número de ocorrência.',
],
[
'Vazios/estrutura de apresentação',
'Não prova alteração de condições jurídicas.',
'Conservar todas as condições comuns e referências; nenhuma validação jurídica automática.',
],
[
'Comprimento dos espaços de preenchimento',
'Não altera a cobertura pedida.',
'Consolidar os cabeçalhos/campos; preservar linhas e proveniências.',
],
[
'Selecção / Seleção',
'Diferença ortográfica, sem alteração da obrigatoriedade indicada na fonte.',
'Usar Seleção na orientação; não impor requisito ao ato sem fundamento específico.',
],
[
'Tabela de fluxo anexada a §13',
'A tabela de 14 etapas foi incluída no intervalo da primeira ocorrência; as ações de governação comuns permanecem.',
'Manter §13 com ações comuns; referenciar separadamente o guia do fluxo. Nomeações/acordos não são executados pelo portal.',
],
];
const esc = (s) =>
String(s)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
let html =
'<!doctype html><html lang="pt"><meta charset="utf-8"><title>Nove diferenças do mestre</title><style>body{font:16px system-ui;margin:2rem}table{width:100%;table-layout:fixed;border-collapse:collapse}td,th{border:1px solid #ccc;padding:12px;vertical-align:top}p{white-space:pre-wrap;overflow-wrap:anywhere}.number{color:#666;font-size:12px}section{margin-bottom:3rem}</style><h1>Nove diferenças lado a lado</h1><p>Extração histórica identificada pelo hash do índice. Não certifica o original atualmente aberto. As ocorrências são A/B, não versões oficiais sucessivas. Sem seleção automática da última ocorrência.</p>';
const summary = [];
diff.forEach((g, i) => {
const a = g.occurrences.map((o) =>
p.filter((x) => x.p >= o.start && x.p <= o.end),
);
html += `<section><h2>${i + 1}. ${esc(g.title)}</h2><p><b>Natureza:</b> ${esc(proposals[i][0])}</p><p><b>Impacto:</b> ${esc(proposals[i][1])}</p><p><b>Proposta fundamentada:</b> ${esc(proposals[i][2])}</p><table><tr>${g.occurrences.map((o) => `<th>Ocorrência p.${o.start}–${o.end}</th>`).join('')}</tr><tr>${a.map((rows) => '<td>' + rows.map((x) => `<p><span class="number">p.${x.p}</span> ${esc(x.text) || '[parágrafo vazio]'}</p>`).join('') + '</td>').join('')}</tr></table></section>`;
summary.push({
section: g.title,
occurrences: g.occurrences,
kind: proposals[i][0],
functionalImpact: proposals[i][1],
proposal: proposals[i][2],
decision:
'compatible_content_preserved; no unresolved behavior contradiction identified in these nine spans',
});
});
fs.writeFileSync(dir + 'nove-diferencas-lado-a-lado.html', html + '</html>');
fs.writeFileSync(
'docs/investmoz/v2/consolidacao-nove-diferencas.json',
JSON.stringify(summary, null, 2),
);
const register = JSON.parse(
fs.readFileSync(dir + 'document-reconciliation-register.json', 'utf8'),
);
const groups = new Map();
for (const r of register.records) {
if (!r.sha256) continue;
const h = r.sha256.toLowerCase();
groups.set(h, [...(groups.get(h) ?? []), r]);
}
const duplicateProposals = [...groups.entries()]
.filter(([, g]) => g.length > 1)
.map(([h, g]) => {
const sorted = [...g].sort((a, b) => {
const score = (x) =>
x.registeredDocuments?.length
? 0
: x.identifier.includes('\\Relatorios\\')
? 1
: x.identifier.length;
return (
score(a) - score(b) || a.identifier.localeCompare(b.identifier)
);
});
return {
sha256: h,
reference: sorted[0].identifier,
allProvenances: g.map((x) => x.identifier),
basis: 'Exact byte equality; prefer registered record, then report archive, then shortest stable identifier. No later-version selection.',
scope: g[0].identifier.includes('::')
? 'embedded_asset_only_not_parent_document'
: 'whole_file',
authorization: 'unchanged_not_inferred',
action: 'proposal_only_no_delete_or_move',
};
});
fs.writeFileSync(
dir + 'duplicate-reference-proposals.json',
JSON.stringify(duplicateProposals, null, 2),
);
const steps = [];
for (let n = 1782; n <= 1847; n += 5) {
const get = (k) => p.find((x) => x.p === k)?.text ?? '';
steps.push({
title: get(n),
inputs: get(n + 2),
sourceParagraph: n,
href:
n === 1797
? '/simulador-fiscal'
: n === 1802 || n === 1807
? '/balcao-virtual/painel'
: '/documentos',
});
}
fs.writeFileSync(
fs.existsSync(dir + 'docx-reopened.json')
? dir + 'fiscal-preparation-guide-historical.ts'
: 'resources/js/lib/fiscal-preparation-guide.ts',
'// Historical source paragraphs; orientation only, never executable legal rules.\nexport const fiscalPreparationGuide = ' +
JSON.stringify(steps, null, 2) +
';\n',
);
console.log(
JSON.stringify({
comparisons: diff.length,
duplicates: duplicateProposals.length,
guideSteps: steps.length,
}),
);
@@ -0,0 +1,170 @@
const fs = require('node:fs');
const crypto = require('node:crypto');
const dir = 'storage/app/private/investmoz-v2/';
const read = (p) =>
JSON.parse(fs.readFileSync(dir + p, 'utf8').replace(/^\uFEFF/, ''));
const inventory = read('legal-journey-inventory.json');
const active = read('document-reconciliation-active.json');
const documents = active.tables.documents;
const resources = active.tables.portal_resources;
const records = [];
for (const item of inventory.entries) {
records.push({
identifier: item.path ?? item.root,
origin: {
inventoryPath: item.path,
status: 'local_provenance_not_official_validation',
},
availability: item.status ?? 'inventoried',
failure: item.reason ?? null,
sha256: item.sha256 ?? null,
version: 'not_identified_from_bytes',
consultationAuthorization: 'not_determined_by_inventory',
legalStatus: 'not_determined_by_inventory',
calculationUse: 'not_activated_by_inventory',
registeredDocuments: documents
.filter(
(d) => d.sha256?.toLowerCase() === item.sha256?.toLowerCase(),
)
.map((d) => ({
metadata: d,
resources: resources
.filter((r) => r.document_id === d.id)
.map((r) => ({
...r,
links: active.tables.portal_resource_links.filter(
(l) => l.resource_id === r.id,
),
})),
})),
});
for (const attachment of item.attachments ?? [])
records.push({
identifier: item.path + '::' + attachment.identifier,
origin: {
parent: item.path,
embeddedIdentifier: attachment.identifier,
},
sha256: attachment.sha256,
availability: 'inventoried_embedded',
version: 'not_identified',
consultationAuthorization: 'not_determined',
legalStatus: 'not_determined',
calculationUse: 'not_activated_by_inventory',
});
}
const groups = new Map();
for (const r of records) {
if (!r.sha256) continue;
const key = r.sha256.toLowerCase();
groups.set(key, [...(groups.get(key) ?? []), r.identifier]);
}
for (const r of records)
r.byteDuplicates = r.sha256
? (groups.get(r.sha256.toLowerCase()) ?? []).filter(
(p) => p !== r.identifier,
)
: [];
fs.writeFileSync(
dir + 'document-reconciliation-register.json',
JSON.stringify(
{
checked_at: new Date().toISOString(),
activeReadOnly: active.target,
records,
},
null,
2,
),
);
function csv(text) {
let rows = [],
row = [],
field = '',
quoted = false;
for (let i = 0; i < text.length; i++) {
let c = text[i];
if (c === '"') {
if (quoted && text[i + 1] === '"') {
field += '"';
i++;
} else quoted = !quoted;
} else if (c === ',' && !quoted) {
row.push(field);
field = '';
} else if (c === '\n' && !quoted) {
row.push(field.replace(/\r$/, ''));
rows.push(row);
row = [];
field = '';
} else field += c;
}
if (field || row.length) {
row.push(field);
rows.push(row);
}
return rows;
}
const index = csv(
fs.readFileSync('docs/investmoz/v2/indice-documento-mestre.csv', 'utf8'),
);
const paragraphs = read('master-paragraphs.json');
const headings = new Map();
for (const row of index.slice(1)) {
const title = row[5];
if (!title) continue;
const start = Number(row[2]),
end = Number(row[3]);
const body = paragraphs
.filter((p) => p.p >= start && p.p <= end)
.map((p) => p.text.replace(/\s+/g, ' ').trim())
.join('\n');
const key = title.trim().toLowerCase();
headings.set(key, [
...(headings.get(key) ?? []),
{
title,
start,
end,
paragraphs: end - start + 1,
contentSha256: crypto
.createHash('sha256')
.update(body)
.digest('hex'),
},
]);
}
const repeated = [...headings.values()]
.filter((g) => g.length > 1)
.map((g) => ({
title: g[0].title,
occurrences: g,
comparison:
new Set(g.map((r) => r.contentSha256)).size > 1
? 'historical_content_differs'
: 'historical_content_identical',
decision: 'pending_human_reconciliation_no_requirement_discarded',
}));
fs.writeFileSync(
'docs/investmoz/v2/repeticoes-reconciliacao.json',
JSON.stringify(
{
sourceSha256: index[1][1],
basis: 'historical_saved_extraction_not_current_locked_original',
repeated,
},
null,
2,
),
);
console.log(
JSON.stringify({
records: records.length,
duplicateGroups: [...groups.values()].filter((g) => g.length > 1)
.length,
repeatedGroups: repeated.length,
differentGroups: repeated.filter(
(g) => g.comparison === 'historical_content_differs',
).length,
}),
);
@@ -0,0 +1,135 @@
const {
chromium,
} = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
(async () => {
const b = await chromium.launch({
headless: true,
executablePath:
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
});
const checks = [];
try {
for (const width of [1440, 390]) {
const c = await b.newContext({ viewport: { width, height: 850 } });
await c.route('**/*', (r) =>
r.request().url().startsWith('http://127.0.0.1:18092')
? r.continue()
: r.abort(),
);
const p = await c.newPage();
const errors = [];
p.on('pageerror', (e) => errors.push(e.message));
for (const path of [
'/documentos',
'/legislacao',
'/como-investir',
]) {
const r = await p.goto('http://127.0.0.1:18092' + path);
assert.equal(
r.headers()['x-investmoz-qa-target'],
'investmoz_v2_20261008_test:55441',
);
const section = p.getByRole('region', {
name: 'Fontes e guias para a jornada',
exact: true,
});
await section.waitFor();
await section
.getByText(
'Guia de preparação fiscal — 14 etapas da fonte',
{ exact: true },
)
.click();
assert.equal(
await section.locator('details ol > li').count(),
14,
);
assert.match(
await section.locator('details').innerText(),
/p\.973–1042/,
);
assert.match(
await section
.locator('details ol > li')
.first()
.innerText(),
/973 \(histórico: 1782\)/,
);
assert.match(
await section.innerText(),
/não concede benefícios/,
);
assert.match(
await section.innerText(),
/estados independentes/,
);
await section
.getByRole('link', {
name: 'Consultar exemplares de formulários',
exact: true,
})
.click();
await p.waitForURL(
(u) =>
u.pathname === '/documentos' &&
u.searchParams.get('document_type') === 'official_form',
);
await p
.getByRole('region', {
name: 'Fontes e guias para a jornada',
exact: true,
})
.getByRole('link', {
name: 'Consultar guias disponíveis',
exact: true,
})
.click();
await p.waitForURL(
(u) => u.searchParams.get('document_type') === 'guide',
);
await p
.getByRole('region', {
name: 'Fontes e guias para a jornada',
exact: true,
})
.getByRole('link', {
name: 'Explorar todo o acervo autorizado',
exact: true,
})
.click();
await p.waitForURL(
(u) => u.pathname === '/documentos' && !u.search,
);
checks.push({
width,
path,
formFilter: true,
guideFilter: true,
unrestrictedLibrary: true,
});
}
assert.deepEqual(errors, []);
await p.screenshot({
path: `storage/app/private/investmoz-v2/legal-journey-${width}.png`,
fullPage: true,
});
await c.close();
}
fs.writeFileSync(
'storage/app/private/investmoz-v2/legal-journey-browser.json',
JSON.stringify(
{ qa: 'investmoz_v2_20261008_test:55441', checks },
null,
2,
),
);
console.log('Legal journey QA navigation passed');
} finally {
await b.close();
}
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});
+127
View File
@@ -0,0 +1,127 @@
const fs = require('fs'),
crypto = require('crypto');
const d = 'storage/app/private/investmoz-v2/';
const r = (f) =>
JSON.parse(fs.readFileSync(d + f, 'utf8').replace(/^\uFEFF/, ''));
const p = r('master-paragraphs-reopened.json'),
source = r('docx-reopened.json');
const heads = p.filter((x) => /^(Ttulo|Title|Heading)/i.test(x.style));
const rows = heads.map((x, i) => ({
title: x.text,
start: x.p,
end: heads[i + 1] ? heads[i + 1].p - 1 : p.length,
style: x.style,
}));
const q = (s) => '"' + String(s).replaceAll('"', '""') + '"';
fs.writeFileSync(
'docs/investmoz/v2/indice-documento-mestre-atual.csv',
[
'fonte,sha256,paragrafo_inicio,paragrafo_fim,estilo,secção,estado',
...rows.map((x) =>
[
'melhorar e transformar o Portal do Investidor atual.docx',
source.sha256,
x.start,
x.end,
x.style,
x.title,
'Leitura atual; implementação individual não certificada',
]
.map(q)
.join(','),
),
].join('\n') + '\n',
);
const groups = new Map();
for (const x of rows) {
const text = p
.filter((v) => v.p >= x.start && v.p <= x.end)
.map((v) => v.text.replace(/\s+/g, ' ').trim())
.join('\n');
const row = {
...x,
contentSha256: crypto.createHash('sha256').update(text).digest('hex'),
};
const key = x.title.trim().toLowerCase();
groups.set(key, [...(groups.get(key) ?? []), row]);
}
const repeated = [...groups.values()]
.filter((g) => g.length > 1)
.map((g) => ({
title: g[0].title,
occurrences: g,
comparison:
new Set(g.map((v) => v.contentSha256)).size > 1
? 'current_content_differs'
: 'current_content_identical',
}));
fs.writeFileSync(
'docs/investmoz/v2/repeticoes-reconciliacao-atual.json',
JSON.stringify(
{ sha256: source.sha256, checked_at: source.checked_at, repeated },
null,
2,
),
);
const changes = r('master-aligned-changes.json');
const map = new Map(changes.mapping.map((v) => [v.old, v.current]));
const historic = r('master-paragraphs.json');
const steps = [];
for (let old = 1782; old <= 1847; old += 5) {
const title = historic.find((x) => x.p === old).text;
const inputs = historic.find((x) => x.p === old + 2).text;
const current = map.get(old);
if (
!current ||
p.find((v) => v.p === current).text !== title ||
p.find((v) => v.p === map.get(old + 2)).text !== inputs
)
throw Error('Guide text changed ' + old);
steps.push({
title,
inputs,
sourceParagraph: current,
historicalSourceParagraph: old,
href:
old === 1797
? '/simulador-fiscal'
: old === 1802 || old === 1807
? '/balcao-virtual/painel'
: '/documentos',
});
}
if (!fs.existsSync(d + 'fiscal-preparation-guide-before-reread.ts'))
fs.copyFileSync(
'resources/js/lib/fiscal-preparation-guide.ts',
d + 'fiscal-preparation-guide-before-reread.ts',
);
fs.writeFileSync(
'resources/js/lib/fiscal-preparation-guide.ts',
'// Current source verified against preserved historical paragraphs; orientation only.\nexport const fiscalPreparationSource = ' +
JSON.stringify({ sha256: source.sha256, start: 973, end: 1042 }) +
';\nexport const fiscalPreparationGuide = ' +
JSON.stringify(steps, null, 2) +
';\n',
);
fs.writeFileSync(
d + 'master-reread-verification.json',
JSON.stringify(
{
sha256: source.sha256,
paragraphs: p.length,
headingCount: heads.length,
repeatedGroups: repeated.length,
differentGroups: repeated.filter(
(g) => g.comparison === 'current_content_differs',
).length,
bodyAdded: changes.added.length,
bodyRemoved: changes.removed.length,
guideStepsVerified: steps.length,
guideTextUnchanged: true,
newFunctionalRequirementsIdentified: false,
},
null,
2,
),
);
console.log(fs.readFileSync(d + 'master-reread-verification.json', 'utf8'));