Files
alla-allaos-fullstack/scripts/generate-pdf-audit-report.ts
phaichayon 51d67ef7c2 task-h.5.3
2026-06-19 17:15:28 +07:00

236 lines
8.4 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,
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<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,
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);
});