pdf task-h

This commit is contained in:
phaichayon
2026-06-19 15:09:58 +07:00
parent a85d24235c
commit fd01ebf7c7
35 changed files with 3905 additions and 133 deletions

View File

@@ -0,0 +1,194 @@
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);
});