114 lines
5.0 KiB
JavaScript
114 lines
5.0 KiB
JavaScript
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.`,
|
|
);
|