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 !== 104) { throw new Error(`R10 changed the stable SendChainService method count: ${facadeMethods.size}`); } const combined = [...domainSources.values()].map((item) => item.source).join('\n'); const downstreamTaskSource = fs.readFileSync( path.join(root, 'api/src/send-chain/send-downstream-requeue-task.service.ts'), 'utf8', ); 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', 'previewDownstreamRequeueTask', 'changeDownstreamRequeueTaskStatus', ]) { if (!(combined + gatewaySubmitSource + downstreamTaskSource + facadeSource).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; ` + '104 stable facade methods and accident invariants preserved.', );