111 lines
4.9 KiB
JavaScript
111 lines
4.9 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 contract = JSON.parse(
|
|
fs.readFileSync(path.join(root, 'docs/contracts/send-chain-r10-completion.json'), 'utf8'),
|
|
);
|
|
const parse = (filePath, source) =>
|
|
ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
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 digest = (value) =>
|
|
crypto.createHash('sha256').update(value.replace(/\r\n/g, '\n')).digest('hex');
|
|
const normalizeFacadeSeams = (value) => value.replaceAll('this.facade.', 'this.');
|
|
|
|
const facadePath = path.join(root, 'api/src/send-chain/send-chain.service.ts');
|
|
const facadeSource = fs.readFileSync(facadePath, 'utf8');
|
|
const facadeFile = parse(facadePath, facadeSource);
|
|
const facadeClass = facadeFile.statements.find(
|
|
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendChainService',
|
|
);
|
|
const completionPath = path.join(root, 'api/src/send-chain/send-completion.service.ts');
|
|
const completionSource = fs.readFileSync(completionPath, 'utf8');
|
|
const completionFile = parse(completionPath, completionSource);
|
|
const completionClass = completionFile.statements.find(
|
|
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendCompletionService',
|
|
);
|
|
if (!facadeClass || !completionClass) throw new Error('R10 facade is missing');
|
|
const facadeMethods = methodsByName(facadeClass);
|
|
const completionMethods = methodsByName(completionClass);
|
|
|
|
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(`R10 domain class missing: ${domain.className}`);
|
|
const methods = methodsByName(classNode);
|
|
const expectedCount = contract.methods.filter((method) => method.file === domain.file).length;
|
|
if (expectedCount !== domain.methodCount) throw new Error(`R10 domain count changed: ${domain.file}`);
|
|
domainSources.set(domain.file, { source, file });
|
|
domainMethods.set(domain.file, methods);
|
|
}
|
|
|
|
for (const expected of contract.methods) {
|
|
const domain = domainSources.get(expected.file);
|
|
const migrated = domainMethods.get(expected.file)?.get(expected.name);
|
|
if (!domain || !migrated?.body) throw new Error(`R10 migrated method missing: ${expected.name}`);
|
|
if (digest(normalizeFacadeSeams(migrated.body.getText(domain.file))) !== expected.bodySha256) {
|
|
throw new Error(`R10 migrated method body changed: ${expected.name}`);
|
|
}
|
|
const completion = completionMethods.get(expected.name);
|
|
if (!completion?.body || !completion.body.getText(completionFile).includes(`.${expected.name}(`)) {
|
|
throw new Error(`R10 completion facade does not delegate: ${expected.name}`);
|
|
}
|
|
const facade = facadeMethods.get(expected.name);
|
|
if (!facade?.body || !facade.body.getText(facadeFile).includes(`this.completion.${expected.name}(`)) {
|
|
throw new Error(`R10 stable facade does not delegate: ${expected.name}`);
|
|
}
|
|
}
|
|
|
|
if (facadeMethods.size !== 98) {
|
|
throw new Error(`R10 changed the stable SendChainService method count: ${facadeMethods.size}`);
|
|
}
|
|
|
|
const combined = [...domainSources.values()].map((item) => item.source).join('\n');
|
|
const gatewaySubmitSource = fs.readFileSync(
|
|
path.join(root, 'api/src/send-chain/send-gateway-submit.service.ts'),
|
|
'utf8',
|
|
);
|
|
for (const invariant of [
|
|
'retryOfSubmitRecordId: sourceAttempt.id',
|
|
'where: { retryOfSubmitRecordId }',
|
|
"error.code === 'P2002'",
|
|
'isCurrentAttempt',
|
|
'sms-reservation-release:${message.messageId}',
|
|
'sms-refund:${message.messageId}',
|
|
'receiptEventKey(data, logicalChannelId)',
|
|
"dedupeKey = data.deliveryType === 'receipt'",
|
|
"status: 'manual_requeueing'",
|
|
'gatewaySubmitRequeueKey',
|
|
'recordReceiptSegment',
|
|
'aggregateReceiptSegments',
|
|
]) {
|
|
if (!(combined + gatewaySubmitSource).includes(invariant)) {
|
|
throw new Error(`R10 accident invariant missing: ${invariant}`);
|
|
}
|
|
}
|
|
if (combined.includes('deleteMany(')) {
|
|
throw new Error('R10 must not delete receipt, retry, accounting or downstream history');
|
|
}
|
|
|
|
const moduleSource = fs.readFileSync(path.join(root, 'api/src/send-chain/send-chain.module.ts'), 'utf8');
|
|
if (!moduleSource.includes('providers: [SendChainService]') || moduleSource.includes('SendCompletionService,')) {
|
|
throw new Error('R10 changed the public NestJS provider boundary');
|
|
}
|
|
|
|
console.log(
|
|
`R10 send completion verified: ${contract.methods.length} methods across ${contract.domains.length} domains; `
|
|
+ '98 stable facade methods and accident invariants preserved.',
|
|
);
|