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

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.`,
);