Files
lislgosms/tools/quality/verify-send-chain-r9.mjs

114 lines
5.0 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
const root = process.cwd();
const servicePath = path.join(root, 'api/src/send-chain/send-chain.service.ts');
const submissionPath = path.join(root, 'api/src/send-chain/send-submission.service.ts');
const contract = JSON.parse(
fs.readFileSync(path.join(root, 'docs/contracts/send-chain-r9-submission.json'), 'utf8'),
);
const serviceSource = fs.readFileSync(servicePath, 'utf8');
const submissionSource = fs.readFileSync(submissionPath, 'utf8');
const parse = (filePath, source) =>
ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
const serviceFile = parse(servicePath, serviceSource);
const submissionFile = parse(submissionPath, submissionSource);
const serviceClass = serviceFile.statements.find(
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendChainService',
);
const submissionClass = submissionFile.statements.find(
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendSubmissionService',
);
if (!serviceClass || !submissionClass) throw new Error('R9 send-chain facade or submission domain is missing');
const methodsByName = (classNode) => new Map(
classNode.members
.filter((member) => ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name))
.map((member) => [member.name.text, member]),
);
const facadeMethods = methodsByName(serviceClass);
const submissionMethods = methodsByName(submissionClass);
const domainSources = new Map();
const domainMethods = new Map();
for (const domain of contract.domains) {
const filePath = path.join(root, 'api/src/send-chain', domain.file);
const source = fs.readFileSync(filePath, 'utf8');
const file = parse(filePath, source);
const classNode = file.statements.find(
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === domain.className,
);
if (!classNode) throw new Error(`R9 domain class missing: ${domain.className}`);
const methods = methodsByName(classNode);
const migratedCount = contract.methods.filter((method) => method.file === domain.file).length;
if (migratedCount !== domain.methodCount) {
throw new Error(`R9 domain contract count changed: ${domain.file}`);
}
domainSources.set(domain.file, { source, file });
domainMethods.set(domain.file, methods);
}
const combinedDomainSource = [...domainSources.values()].map((item) => item.source).join('\n');
const digest = (value) =>
crypto.createHash('sha256').update(value.replace(/\r\n/g, '\n')).digest('hex');
const normalizeFacadeSeams = (value) => value.replaceAll('this.facade.', 'this.');
for (const expected of contract.methods) {
const domain = domainSources.get(expected.file);
const migrated = domainMethods.get(expected.file)?.get(expected.name);
if (!migrated?.body) throw new Error(`R9 migrated method missing: ${expected.name}`);
if (digest(normalizeFacadeSeams(migrated.body.getText(domain.file))) !== expected.bodySha256) {
throw new Error(`R9 migrated method body changed: ${expected.name}`);
}
const internalFacade = submissionMethods.get(expected.name);
if (!internalFacade?.body || !internalFacade.body.getText(submissionFile).includes(`.${expected.name}(`)) {
throw new Error(`R9 internal submission facade does not delegate ${expected.name}`);
}
const facade = facadeMethods.get(expected.name);
if (!facade?.body) throw new Error(`R9 stable facade method missing: ${expected.name}`);
const body = facade.body.getText(serviceFile);
if (!body.includes(`this.submission.${expected.name}(`)) {
throw new Error(`R9 facade does not delegate ${expected.name}`);
}
}
for (const retained of [
'handleSubmitSegmentResult',
'handleSubmitResult',
'intakeReceipt',
'handleReceipt',
'retryMessageIfAllowed',
'recordCmppFailureReceipt',
'refundMessage',
'queueAndTryDownstreamDelivery',
]) {
const method = facadeMethods.get(retained);
if (!method?.body || method.body.getText(serviceFile).includes('this.submission.')) {
throw new Error(`R9 moved R10 behavior out of the facade: ${retained}`);
}
}
for (const invariant of [
"new Logger('SendChainService')",
'this.prisma.$transaction',
'this.callbacks.releaseMessageReservation',
'this.callbacks.recordCmppFailureReceipt',
'GATEWAY_SUBMIT_STREAM',
'await this.facade.publishGatewaySubmitCommand(command);',
'new Worker<SendJob>(',
]) {
if (!combinedDomainSource.includes(invariant)) {
throw new Error(`R9 submission invariant missing: ${invariant}`);
}
}
const moduleSource = fs.readFileSync(path.join(root, 'api/src/send-chain/send-chain.module.ts'), 'utf8');
if (!moduleSource.includes('providers: [SendChainService]') || moduleSource.includes('SendSubmissionService,')) {
throw new Error('R9 changed the public NestJS provider boundary');
}
console.log(
`R9 send submission verified: ${contract.methods.length} methods across ${contract.domains.length} domains; `
+ `${facadeMethods.size} stable facade methods, unchanged bodies and R10 boundaries preserved.`,
);