learning_ai_invt_trdg/backend/verifySchemaContract.ts

54 lines
2.9 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = __dirname;
const schemaDir = path.join(repoRoot, 'schema');
const schemaFiles = fs
.readdirSync(schemaDir)
.filter((name) => name.endsWith('.sql'))
.sort();
const combinedSql = schemaFiles
.map((name) => fs.readFileSync(path.join(schemaDir, name), 'utf8').toLowerCase())
.join('\n');
function assertSql(pattern: RegExp, message: string) {
assert(pattern.test(combinedSql), message);
}
// Core tables used by runtime.
assertSql(/\bcreate\s+table\s+if\s+not\s+exists\s+trade_profiles\b/, 'Missing trade_profiles table definition');
assertSql(/\bcreate\s+table\s+if\s+not\s+exists\s+bot_config\b/, 'Missing bot_config table definition');
assertSql(/\bcreate\s+table\s+if\s+not\s+exists\s+dynamic_config\b/, 'Missing dynamic_config table definition');
// Orders columns controlled by local migrations.
assertSql(/\balter\s+table\s+orders\b/, 'orders migration statements missing');
assertSql(/\borders\b[\s\S]*\bprofile_id\b/, 'orders.profile_id not found in migration contract');
assertSql(/\borders\b[\s\S]*\bstop_loss\b/, 'orders.stop_loss not found in migration contract');
assertSql(/\borders\b[\s\S]*\btake_profit\b/, 'orders.take_profit not found in migration contract');
assertSql(/\borders\b[\s\S]*\baction\b/, 'orders.action not found in migration contract');
assertSql(/\borders\b[\s\S]*\btrade_id\b/, 'orders.trade_id not found in migration contract');
assertSql(/\borders\b[\s\S]*\bsub_tag\b/, 'orders.sub_tag not found in migration contract');
// Capital ledger reserve formula must include realized_pnl to match runtime available-capital math.
assertSql(
/\bcreate\s+or\s+replace\s+function\s+fn_reserve_for_order\b[\s\S]*allocated_capital\s*\+\s*realized_pnl[\s\S]*reserved_for_orders[\s\S]*reserved_for_positions[\s\S]*>=\s*p_amount/,
'fn_reserve_for_order must gate reservation using allocated_capital + realized_pnl - reserved balances'
);
// Trade history columns used by dashboard/bot traceability.
assertSql(/\btrade_history\b[\s\S]*\bprofile_id\b/, 'trade_history.profile_id not found in migration contract');
assertSql(/\btrade_history\b[\s\S]*\brules_metadata\b/, 'trade_history.rules_metadata not found in migration contract');
assertSql(/\btrade_history\b[\s\S]*\btrade_id\b/, 'trade_history.trade_id not found in migration contract');
assertSql(/\btrade_history\b[\s\S]*\bsource\b/, 'trade_history.source not found in migration contract');
// RLS presence for critical user-scoped tables.
assertSql(/\balter\s+table\s+trade_profiles\s+enable\s+row\s+level\s+security\b/, 'trade_profiles RLS enable statement missing');
assertSql(/\bcreate\s+policy\b[\s\S]*trade_profiles\b/, 'trade_profiles policy definition missing');
console.log(`[schema-contract] OK: validated ${schemaFiles.length} schema files`);