195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
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,
|
|
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 { 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);
|
|
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 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<string, unknown> | 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,
|
|
duplicateFields,
|
|
unknownFields
|
|
},
|
|
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,
|
|
itemTableColumns: itemTableColumns.map((column) => ({
|
|
columnName: column.columnName,
|
|
sourceField: column.sourceField
|
|
}))
|
|
},
|
|
runtimePayload: {
|
|
templateInputKeys: Object.keys(payload.templateInput).length,
|
|
topicInputKeys: Object.keys(payload.topicInputs).length
|
|
},
|
|
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}`,
|
|
'',
|
|
'## 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);
|
|
});
|