import { writeFile } from 'node:fs/promises'; import fs from 'node:fs'; import path from 'node:path'; import { AUDIT_ARTIFACTS_DIR, AUDIT_QUOTATION_CODE, REPORT_JSON_PATH, REPORT_MARKDOWN_PATH, buildAuditPayload, classifyTemplateField, computeFieldDiff, createSqlClient, extractTemplateFields, getEffectiveMappedKeys, getDuplicateFieldNames, getMappingsForVersion, getUnknownFieldNames, summarizeIssues } from './pdf-audit-utils.ts'; async function main() { const sql = createSqlClient(); try { const payload = await buildAuditPayload(sql); const fields = extractTemplateFields(payload.templateSchema); const previousVersion = ( await sql` select schema_json as "schemaJson", version from crm_document_template_versions where template_id = ( select template_id from crm_document_template_versions where id = ${payload.templateVersionId} limit 1 ) and id <> ${payload.templateVersionId} and deleted_at is null order by created_at desc limit 1 ` )[0] ?? null; const previousFields = previousVersion ? extractTemplateFields(previousVersion.schemaJson) : []; const { mappings, columns } = await getMappingsForVersion(sql, payload.templateVersionId); const fieldNames = new Set(fields.map((field) => field.name).filter(Boolean)); const placeholderTokens = [...new Set(fields.flatMap((field) => field.tokens))].sort(); const mappedKeys = getEffectiveMappedKeys(fields, mappings); const unmappedTokens = placeholderTokens.filter((token) => { if (token === 'item_topic') { return false; } if (token === 'topic' || token === 'data_topic') { return false; } if (token.startsWith('topic_') || token.startsWith('item_topic_')) { return false; } return !mappedKeys.has(token); }); const orphanMappings = mappings .filter( (mapping) => !placeholderTokens.includes(mapping.placeholderKey) && !fieldNames.has(mapping.placeholderKey) && mapping.placeholderKey !== 'items_table' ) .map((mapping) => mapping.placeholderKey); const duplicateFields = getDuplicateFieldNames(fields); const unknownFields = getUnknownFieldNames(fields, mappedKeys); const fieldDiff = computeFieldDiff(fields, previousFields); const classifications = fields.map((field) => ({ name: field.name, classification: classifyTemplateField(field, mappedKeys) })); const topicRows = Object.keys(payload.topicInputs).filter((key) => key.startsWith('topic_')).length; const topicItemRows = Object.keys(payload.topicInputs).filter((key) => key.startsWith('item_topic_') ).length; const itemTableMapping = mappings.find((mapping) => mapping.placeholderKey === 'items_table') ?? null; const priceTableMapping = mappings.find((mapping) => mapping.placeholderKey === 'quotation_price_data') ?? null; const itemTableColumns = itemTableMapping ? columns.filter((column) => column.mappingId === itemTableMapping.id) : []; const snapshotTemplateInput = payload.approvedSnapshot && typeof payload.approvedSnapshot === 'object' && 'templateInput' in payload.approvedSnapshot ? (payload.approvedSnapshot.templateInput as Record | null) : null; const sharedKeys = snapshotTemplateInput ? Object.keys(payload.templateInput).filter( (key) => !(key.startsWith('topic_') || key.startsWith('item_topic_')) && key in snapshotTemplateInput ) : []; const inconsistentKeys = sharedKeys.filter( (key) => JSON.stringify(payload.templateInput[key]) !== JSON.stringify(snapshotTemplateInput?.[key]) ); const consistencyStatus = snapshotTemplateInput ? inconsistentKeys.length ? 'FAIL' : 'PASS' : 'WARNING'; const overallStatus = summarizeIssues([ unmappedTokens.length ? 'FAIL' : 'PASS', orphanMappings.length ? 'WARNING' : 'PASS', duplicateFields.length ? 'FAIL' : 'PASS', unknownFields.length ? 'WARNING' : 'PASS', consistencyStatus ]); const report = { generatedAt: new Date().toISOString(), overallStatus, quotationCode: payload.quotationCode, expectedFixtureCode: AUDIT_QUOTATION_CODE, template: { templateName: payload.templateName, version: payload.templateVersion, versionId: payload.templateVersionId, fieldCount: fields.length, previousVersion: previousVersion?.version ?? null, deletedFields: fieldDiff.deletedFields, newFields: fieldDiff.newFields, duplicateFields, unknownFields, classifications }, metrics: { topicCount: payload.quotationMetrics.topicCount, itemCount: payload.quotationMetrics.itemCount, partyCount: payload.quotationMetrics.partyCount, dynamicTopicFieldCount: topicRows, dynamicTopicItemFieldCount: topicItemRows, mappingCount: mappings.length }, mappingCoverage: { placeholderTokenCount: placeholderTokens.length, unmappedTokens, orphanMappings, priceTableMapping: priceTableMapping ? { sourcePath: priceTableMapping.sourcePath, dataType: priceTableMapping.dataType } : null, itemTableColumns: itemTableColumns.map((column) => ({ columnName: column.columnName, sourceField: column.sourceField })) }, runtimePayload: { templateInputKeys: Object.keys(payload.templateInput).length, topicInputKeys: Object.keys(payload.topicInputs).length, priceTableRows: Array.isArray(payload.templateInput.quotation_price_data) ? payload.templateInput.quotation_price_data.length : 0 }, consistency: { status: consistencyStatus, approvedSnapshotAvailable: Boolean(snapshotTemplateInput), approvedArtifactReference: payload.approvedArtifactReference, inconsistentKeys }, integritySummary: { available: fs.existsSync(path.resolve(AUDIT_ARTIFACTS_DIR, 'summary.json')), artifactDir: AUDIT_ARTIFACTS_DIR } }; const markdown = [ '# PDF Audit Report', '', `- Generated At: ${report.generatedAt}`, `- Overall Status: ${report.overallStatus}`, `- Quotation Code: ${report.quotationCode}`, `- Template: ${report.template.templateName} v${report.template.version}`, `- Previous Version: ${report.template.previousVersion ?? 'None'}`, '', '## Metrics', '', `- Topic Count: ${report.metrics.topicCount}`, `- Item Count: ${report.metrics.itemCount}`, `- Project Party Count: ${report.metrics.partyCount}`, `- Mapping Count: ${report.metrics.mappingCount}`, '', '## Mapping Coverage', '', `- Placeholder Tokens: ${report.mappingCoverage.placeholderTokenCount}`, `- Unmapped Tokens: ${report.mappingCoverage.unmappedTokens.length ? report.mappingCoverage.unmappedTokens.join(', ') : 'None'}`, `- Orphan Mappings: ${report.mappingCoverage.orphanMappings.length ? report.mappingCoverage.orphanMappings.join(', ') : 'None'}`, '', '## Visual / Consistency', '', `- Approved Snapshot Available: ${report.consistency.approvedSnapshotAvailable ? 'Yes' : 'No'}`, `- Consistency Status: ${report.consistency.status}`, `- Inconsistent Keys: ${report.consistency.inconsistentKeys.length ? report.consistency.inconsistentKeys.join(', ') : 'None'}`, '', '## Integrity Layer', '', `- Integrity Summary Available: ${report.integritySummary.available ? 'Yes' : 'No'}`, `- Artifact Directory: ${report.integritySummary.artifactDir}` ].join('\n'); await writeFile(REPORT_JSON_PATH, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); await writeFile(REPORT_MARKDOWN_PATH, `${markdown}\n`, 'utf8'); console.log( JSON.stringify( { audit: 'pdf-report', overallStatus, jsonPath: REPORT_JSON_PATH, markdownPath: REPORT_MARKDOWN_PATH, report }, null, 2 ) ); } finally { await sql.end({ timeout: 5 }); } } main().catch((error) => { console.error(`[generate-pdf-audit-report] ${error.message}`); process.exit(1); });