274 lines
9.9 KiB
JavaScript
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,
|
|
},
|
|
),
|
|
);
|