import { PDFME_STATIC_LABEL_FIELD_KEYS, buildAuditPayload, createSqlClient, summarizeIssues, writeAuditArtifact } from './pdf-audit-utils.ts'; type PayloadFinding = { field: string; source: 'documentData' | 'templateInput' | 'topicInputs'; status: 'PASS' | 'WARNING' | 'FAIL'; value: unknown; message: string; }; function pushFinding(findings: PayloadFinding[], finding: PayloadFinding) { findings.push(finding); } function walkValue( findings: PayloadFinding[], source: PayloadFinding['source'], prefix: string, value: unknown ) { if (value === undefined) { pushFinding(findings, { field: prefix, source, status: 'FAIL', value, message: 'Value is undefined' }); return; } if (value === null) { pushFinding(findings, { field: prefix, source, status: 'WARNING', value, message: 'Value is null' }); return; } if (typeof value === 'string') { pushFinding(findings, { field: prefix, source, status: value.trim() ? 'PASS' : 'WARNING', value, message: value.trim() ? 'String is populated' : 'String is empty' }); return; } if (Array.isArray(value)) { if (source === 'documentData' && value.some((row) => !Array.isArray(row))) { pushFinding(findings, { field: prefix, source, status: 'PASS', value: `rows:${value.length}`, message: 'Collection payload is populated' }); for (const [index, item] of value.entries()) { walkValue(findings, source, `${prefix}[${index}]`, item); } return; } const validTable = value.every( (row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string') ); pushFinding(findings, { field: prefix, source, status: validTable ? 'PASS' : 'FAIL', value: Array.isArray(value) ? `rows:${value.length}` : value, message: validTable ? 'Array shape is valid' : 'Array shape is invalid for PDF payload' }); for (const [index, item] of value.entries()) { if (Array.isArray(item)) { for (const [cellIndex, cell] of item.entries()) { if (typeof cell !== 'string') { pushFinding(findings, { field: `${prefix}[${index}][${cellIndex}]`, source, status: 'FAIL', value: cell, message: 'Table cell is not a string' }); } } } } return; } if (typeof value === 'object') { for (const [key, childValue] of Object.entries(value)) { walkValue(findings, source, prefix ? `${prefix}.${key}` : key, childValue); } return; } pushFinding(findings, { field: prefix, source, status: 'PASS', value, message: 'Scalar value is populated' }); } async function main() { const sql = createSqlClient(); try { const payload = await buildAuditPayload(sql); const findings: PayloadFinding[] = []; walkValue(findings, 'documentData', 'documentData', payload.documentData); walkValue(findings, 'templateInput', 'templateInput', payload.templateInput); walkValue(findings, 'topicInputs', 'topicInputs', payload.topicInputs); const priceTable = payload.templateInput.quotation_price_data; const priceTableRows = Array.isArray(priceTable) ? priceTable : []; const firstPriceRow = Array.isArray(priceTableRows[0]) ? (priceTableRows[0] as unknown[]) : []; const itemsTable = payload.templateInput.items_table; const itemsTableRows = Array.isArray(itemsTable) ? itemsTable : []; const firstItemsRow = Array.isArray(itemsTableRows[0]) ? (itemsTableRows[0] as unknown[]) : []; pushFinding(findings, { field: 'templateInput.quotation_price_data', source: 'templateInput', status: Array.isArray(priceTable) ? 'PASS' : 'FAIL', value: Array.isArray(priceTable) ? `rows:${priceTable.length}` : priceTable, message: Array.isArray(priceTable) ? 'Price table input is present' : 'Price table input is missing or not a table' }); pushFinding(findings, { field: 'templateInput.quotation_price_data[0][0]', source: 'templateInput', status: firstPriceRow[0] === 'Price (Exclude VAT)' ? 'PASS' : 'FAIL', value: firstPriceRow[0] ?? null, message: 'Price table label matches canonical text' }); pushFinding(findings, { field: 'templateInput.quotation_price_data[0][1]', source: 'templateInput', status: typeof firstPriceRow[1] === 'string' && firstPriceRow[1].trim() && firstPriceRow[1] !== '-' ? 'PASS' : 'FAIL', value: firstPriceRow[1] ?? null, message: 'Price table amount cell is formatted' }); pushFinding(findings, { field: 'templateInput.quotation_price_data[0][3]', source: 'templateInput', status: typeof firstPriceRow[3] === 'string' && firstPriceRow[3].trim() ? 'PASS' : 'FAIL', value: firstPriceRow[3] ?? null, message: 'Price table currency code cell is populated' }); if (itemsTable !== undefined) { pushFinding(findings, { field: 'templateInput.items_table', source: 'templateInput', status: Array.isArray(itemsTable) ? 'PASS' : 'FAIL', value: Array.isArray(itemsTable) ? `rows:${itemsTable.length}` : itemsTable, message: Array.isArray(itemsTable) ? 'Product items table input is present' : 'Product items table input is missing or not table' }); pushFinding(findings, { field: 'templateInput.items_table[0]', source: 'templateInput', status: firstItemsRow.length === 7 ? 'PASS' : 'FAIL', value: firstItemsRow.length, message: 'Product items table row shape uses seven columns' }); } const staticLabelFindings = [...PDFME_STATIC_LABEL_FIELD_KEYS].map((fieldName) => { const value = payload.templateInput[fieldName]; const valid = typeof value === 'string' && value.trim().length > 0 && value !== '-'; const finding: PayloadFinding = { field: `templateInput.${fieldName}`, source: 'templateInput', status: valid ? 'PASS' : 'FAIL', value: value ?? null, message: valid ? 'Static label is populated' : 'Static label is empty' }; pushFinding(findings, finding); return finding; }); const status = findings.some((finding) => finding.status === 'FAIL') ? 'FAIL' : 'PASS'; const report = { audit: 'runtime-payload', overallStatus: status, staticLabelsStatus: summarizeIssues( staticLabelFindings.map((finding) => finding.status) ), quotationCode: payload.quotationCode, templateName: payload.templateName, templateVersion: payload.templateVersion, findingCount: findings.length, findings }; const artifactPath = await writeAuditArtifact('runtime-payload-report.json', report); console.log(JSON.stringify({ ...report, artifactPath }, null, 2)); } finally { await sql.end({ timeout: 5 }); } } main().catch((error) => { console.error(`[audit-pdf-runtime-payload] ${error.message}`); process.exit(1); });