import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import ts from 'typescript'; const root = process.cwd(); const contract = JSON.parse(fs.readFileSync(path.join(root, 'docs', 'contracts', 'send-chain-r8-pure-logic.json'), 'utf8')); const servicePath = path.join(root, 'api', 'src', 'send-chain', 'send-chain.service.ts'); const contractsPath = path.join(root, 'api', 'src', 'send-chain', 'send-chain.contracts.ts'); const helpersPath = path.join(root, 'api', 'src', 'send-chain', 'send-chain.helpers.ts'); const service = fs.readFileSync(servicePath, 'utf8'); const contracts = fs.readFileSync(contractsPath, 'utf8'); const helpers = fs.readFileSync(helpersPath, 'utf8'); const implementationSources = [ service, ...fs.readdirSync(path.dirname(servicePath)) .filter((fileName) => /^send-.*\.service\.ts$/.test(fileName) && fileName !== 'send-chain.service.ts') .map((fileName) => fs.readFileSync(path.join(path.dirname(servicePath), fileName), 'utf8')), ].join('\n'); const parse = (filePath, source) => ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); const contractFile = parse(contractsPath, contracts); const helperFile = parse(helpersPath, helpers); const serviceFile = parse(servicePath, service); const digest = (text) => crypto.createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex'); const normalizedText = (source, file, node) => source.slice(node.getStart(file), node.end).trim().replace(/^export\s+/, ''); const contractDeclarations = new Map(); for (const statement of contractFile.statements) { if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) { contractDeclarations.set(statement.name.text, statement); } } for (const expected of contract.contracts) { const node = contractDeclarations.get(expected.name); if (!node) throw new Error(`R8 contract missing: ${expected.name}`); const actualHash = digest(normalizedText(contracts, contractFile, node)); if (actualHash !== expected.sha256) { throw new Error(`R8 contract changed: ${expected.name}`); } } const helperDeclarations = new Map(); for (const statement of helperFile.statements) { if (ts.isFunctionDeclaration(statement) && statement.name) { helperDeclarations.set(statement.name.text, statement); } else if (ts.isVariableStatement(statement)) { const name = statement.declarationList.declarations[0]?.name; if (name && ts.isIdentifier(name)) helperDeclarations.set(name.text, statement); } } for (const expected of contract.helpers) { const node = helperDeclarations.get(expected.name); if (!node) throw new Error(`R8 helper missing: ${expected.name}`); const actualHash = digest(normalizedText(helpers, helperFile, node)); if (actualHash !== expected.sha256) { throw new Error(`R8 helper changed: ${expected.name}`); } } const serviceClass = serviceFile.statements.find( (statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendChainService', ); if (!serviceClass) throw new Error('R8 stable SendChainService missing'); const serviceMethods = serviceClass.members.filter(ts.isMethodDeclaration); if (serviceMethods.length < 60) throw new Error(`R8 service lost orchestration methods: ${serviceMethods.length}`); for (const forbidden of serviceFile.statements) { if (ts.isFunctionDeclaration(forbidden) || ts.isInterfaceDeclaration(forbidden) || ts.isTypeAliasDeclaration(forbidden)) { throw new Error('R8 service still contains movable top-level declarations'); } } for (const invariant of [ 'this.prisma.$transaction', 'publishGatewaySubmitCommand', 'getSendQueue()', 'getGatewayQueue()', 'retryMessageIfAllowed', 'recordSubmitSegments', 'recordReceiptSegment', ]) { if (!implementationSources.includes(invariant)) throw new Error(`R8 side-effect invariant missing: ${invariant}`); } for (const helper of [ 'selectChannelCandidate', 'aggregateReceiptSegmentState', 'isSameUpstreamEndpointIdentity', 'receiptEventKey', ]) { if (!helperDeclarations.has(helper)) throw new Error(`R8 extracted policy missing: ${helper}`); } for (const controller of [ 'admin-send-chain.controller.ts', 'client-send-chain.controller.ts', 'gateway-events.controller.ts', ]) { const source = fs.readFileSync(path.join(root, 'api', 'src', 'send-chain', controller), 'utf8'); if (!source.includes("from './send-chain.contracts'")) { throw new Error(`R8 controller does not use contracts: ${controller}`); } } const spec = fs.readFileSync(path.join(root, 'api', 'src', 'send-chain', 'send-chain.helpers.spec.ts'), 'utf8'); for (const phrase of ['prefers an approved online province channel', 'keeps a segmented message non-terminal', 'generates a stable receipt event key']) { if (!spec.includes(phrase)) throw new Error(`R8 pure policy test missing: ${phrase}`); } console.log( `R8 send-chain pure logic verified: ${contract.contracts.length} contracts, ${contract.helpers.length} migrated declarations, ` + `${serviceMethods.length} orchestration methods and 4 extracted policies preserved.`, );