Files
Investidor-V2/tools/portal-audit-legacy-manual-update.mjs
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

274 lines
9.9 KiB
JavaScript

// Edit only the reported manual criteria in the sole matrix; preserve every other cell.
import fs from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { pathToFileURL, fileURLToPath } from 'node:url';
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const DOC = path.join(ROOT, 'docs/investmoz/v2/auditoria-portal');
const RUNTIME = path.join(
ROOT,
'storage/app/private/investmoz-v2/portal-audit/legacy-sector-reconciliation-20261010',
);
const requireRuntime = createRequire(path.join(RUNTIME, 'package.json'));
const { Workbook } = await import(
pathToFileURL(requireRuntime.resolve('@oai/artifact-tool')).href
);
const sourcesOnly = process.argv.includes('--sources-only');
const baseline = JSON.parse(
await fs.readFile(
path.join(
RUNTIME,
sourcesOnly ? 'csv-source-baseline.json' : 'csv-baselines.json',
),
'utf8',
),
);
const reconciliation = JSON.parse(
await fs.readFile(path.join(RUNTIME, 'reconciliation.json'), 'utf8'),
);
const csvUpdates = sourcesOnly
? {
'inventario-fontes.csv': JSON.parse(
await fs.readFile(
path.join(RUNTIME, 'source-inventory-updates.json'),
'utf8',
),
),
}
: reconciliation.csv_updates;
const manualTag = '[Confirmação manual recebida 10/10/2026] ';
const scope = {
'AUD-LEG-03': [
'leitura_publica',
'autenticacao',
'criar_ou_escolher_dossier',
'guardar_referencia',
'nova_sessao',
'recuperar_dossier',
'abrir_artigo',
],
'AUD-LEG-04': ['leitura_publica', 'autenticacao'],
'AUD-LEG-06': [
'criar_ou_escolher_dossier',
'guardar_referencia',
'nova_sessao',
'recuperar_dossier',
'abrir_artigo',
],
'AUD-LEG-07': ['guardar_referencia', 'recuperar_dossier', 'abrir_artigo'],
};
const evidence =
'Responsável confirmou leitura pública → autenticação → criar/escolher dossier → guardar referência → nova sessão → recuperar dossier e abrir artigo. Confirmação declarada, sem conta/perfil/dispositivo/data de execução identificados. Não comprova MFA específico, ensaios móveis, acessibilidade, outros perfis, deduplicação, C37/QA18 ou A/B. Ver legado-piloto-qa.md, confirmação manual.';
const allowedColumns = [
'Evidência funcional',
'Interface',
'Gravação',
'Recuperação',
'Resultado apresentado',
'Consolidação',
'Critérios e versões por origem (JSON)',
];
const outputs = [];
function append(original, value) {
return original.includes(manualTag)
? original
: original + ' | ' + manualTag + value;
}
function encodeCSV(rows) {
const quote = (value) =>
/[",\r\n]/.test(value)
? '"' + value.replaceAll('"', '""') + '"'
: value;
return (
'\ufeff' +
rows.map((row) => row.map(quote).join(',')).join('\r\n') +
'\r\n'
);
}
function matchesSource(value, literal) {
// Imported ISO dates are typed by this runtime. Compare their instant for validation,
// but serialize untouched fields from the original CSV so its exact offset survives.
if (value instanceof Date) return value.getTime() === Date.parse(literal);
return (value == null ? '' : String(value)) === literal;
}
for (const filename of sourcesOnly
? ['inventario-fontes.csv']
: [
'matriz-requisitos.csv',
'legado-inventario.csv',
'legado-conteudos-reconciliados.csv',
]) {
const input = await fs.readFile(path.join(DOC, filename), 'utf8');
const sourceRows = baseline[filename];
const workbook = await Workbook.fromCSV(input.replace(/^\ufeff/, ''), {
sheetName: 'Registos',
});
const sheet = workbook.worksheets.getItem('Registos');
const fullRange = sheet.getRangeByIndexes(
0,
0,
sourceRows.length,
sourceRows[0].length,
);
const rows = fullRange.values;
assert(
rows.every((row, i) =>
row.every((value, c) => matchesSource(value, sourceRows[i][c])),
),
'Imported data must match source values',
);
const strings = sourceRows.map((row) => [...row]);
const headers = strings[0];
const changes = [];
for (let i = 1; i < strings.length; i++) {
const original = strings[i];
const updated = [...original];
const id =
filename === 'matriz-requisitos.csv'
? original[headers.indexOf('ID')]
: null;
if (id && scope[id]) {
for (const column of allowedColumns.filter(
(c) => c !== 'Critérios e versões por origem (JSON)',
)) {
const index = headers.indexOf(column);
updated[index] = append(original[index], evidence);
}
const index = headers.indexOf(
'Critérios e versões por origem (JSON)',
);
const variants = JSON.parse(original[index]);
for (const variant of variants) {
variant.confirmacao_manual = {
recebida_em: '2026-10-10',
fonte: 'Mensagem do responsável nesta conversa',
conta: 'não indicada',
perfil: 'não indicado',
dispositivo: 'não indicado',
criterios: scope[id].map((criterio) => ({
criterio,
estado: 'confirmado manualmente',
ambito: 'piloto agronegócio QA',
})),
exclusoes: [
'móvel',
'acessibilidade',
'outros perfis',
'C37 conta18',
'cenários A/B',
'cobertura integral',
],
};
}
updated[index] = JSON.stringify(variants);
} else if (filename !== 'matriz-requisitos.csv') {
const prepared = csvUpdates[filename][i - 1];
assert(prepared);
for (let c = 0; c < headers.length; c++) {
if (prepared[headers[c]] !== original[c]) {
const allowed =
filename === 'legado-inventario.csv'
? ['Destino proposto', 'Situação']
: filename === 'inventario-fontes.csv'
? [
'SHA256',
'Bytes',
'Nota de leitura',
'Edição/versão identificada',
]
: ['Situação'];
assert(
allowed.includes(headers[c]),
'Source identity, authors, dates, hashes and URL aliases are immutable',
);
updated[c] = prepared[headers[c]];
}
}
}
if (JSON.stringify(original) !== JSON.stringify(updated)) {
sheet.getRangeByIndexes(i, 0, 1, headers.length).values = [updated];
changes.push({
row: i + 1,
id,
columns: headers.filter((_, c) => original[c] !== updated[c]),
});
strings[i] = updated;
}
}
workbook.recalculate();
const observed = sheet.getRangeByIndexes(
0,
0,
sourceRows.length,
headers.length,
).values;
assert(
observed.every((row, i) =>
row.every((value, c) => matchesSource(value, strings[i][c])),
),
'Authored fields must match verified values',
);
const final = strings;
for (let i = 0; i < sourceRows.length; i++) {
for (let c = 0; c < headers.length; c++) {
if (sourceRows[i][c] === final[i][c]) continue;
const id =
filename === 'matriz-requisitos.csv' ? sourceRows[i][0] : null;
assert(
changes.some(
(change) =>
change.row === i + 1 &&
change.columns.includes(headers[c]),
),
);
if (id) assert(scope[id] && allowedColumns.includes(headers[c]));
}
}
// CSV has no workbook layout, formulas or styles; serialize the verified cell strings
// in the repository's established BOM/CRLF CSV format rather than adding an XLSX matrix.
const output = encodeCSV(final);
const temp = path.join(RUNTIME, filename + '.verified');
await fs.writeFile(temp, output, 'utf8');
outputs.push({ filename, temp, changes });
}
// Apply only after ALL three imported tables have passed exact preservation checks.
for (const output of outputs)
await fs.copyFile(output.temp, path.join(DOC, output.filename));
await fs.writeFile(
path.join(
RUNTIME,
sourcesOnly ? 'csv-source-update-proof.json' : 'csv-update-proof.json',
),
JSON.stringify(
{
database_access: false,
changed: outputs.map(({ filename, changes }) => ({
filename,
changes,
})),
},
null,
2,
),
);
console.log(
JSON.stringify(
sourcesOnly
? {
source_metadata_rows_updated: outputs[0].changes.length,
database_writes: 0,
}
: {
matrix_rows_changed: outputs[0].changes.map((c) => c.id),
statuses_changed: 0,
inventory_rows_reconciled: outputs[1].changes.length,
article_versions_reconciled: outputs[2].changes.length,
database_writes: 0,
},
),
);