morphit/apps/indexer/scripts/reputation-decay-sql-constant-parity-smoke.ts

95 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
/*
* reputation-decay-sql-constant-parity — cp175 F-011 guard.
*
* The reputation time-decay weight is defined ONCE as a constant +
* helper in apps/indexer/src/indexer/reputation/decay.ts
* (REPUTATION_DECAY_HALF_LIFE_DAYS = 365, and reputationDecayWeightSql()).
* But the SQL formula `POWER(0.5, EXTRACT(EPOCH FROM (NOW() - <col>)) /
* (365 * 86400.0))` is hand-INLINED in the live query files
* (api/feedback.ts ×6, api/orderbook.ts, api/orderbookStream.ts) with the
* 365 as a magic number — and reputationDecayWeightSql() is currently
* unused (F-011).
*
* The risk: the JS implementation (reputationDecayWeight, used by the
* verifiable-receipt endpoint so readers re-derive scores locally) IS
* guarded against the constant by reputation-decay-smoke. The SQL path is
* NOT. If someone changed REPUTATION_DECAY_HALF_LIFE_DAYS, the JS receipt
* and the SQL rating query would silently diverge — a user would compute a
* different reputation locally than the server displays.
*
* This sentinel asserts every inlined decay half-life literal in the API
* SQL equals REPUTATION_DECAY_HALF_LIFE_DAYS, so the duplication can't drift.
*/
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { REPUTATION_DECAY_HALF_LIFE_DAYS } from '../src/indexer/reputation/decay';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = resolve(HERE, '..', 'src');
const FILES = ['api/feedback.ts', 'api/orderbook.ts', 'api/orderbookStream.ts'];
// Match the inlined decay denominator: `(<N> * 86400` where N is the
// half-life-in-days literal baked into the SQL.
const DECAY_LITERAL_RE = /\((\d+)\s*\*\s*86400(?:\.0)?\)/g;
let pass = 0;
let fail = 0;
function ok(name: string): void {
console.log(`${name}`);
pass++;
}
function bad(name: string, detail: string): void {
console.log(`${name}: ${detail}`);
fail++;
}
console.log('\n── reputation-decay-sql-constant-parity (cp175 F-011 guard) ──\n');
console.log(` (canonical REPUTATION_DECAY_HALF_LIFE_DAYS = ${REPUTATION_DECAY_HALF_LIFE_DAYS})\n`);
let totalLiterals = 0;
let mismatches = 0;
for (const rel of FILES) {
const src = readFileSync(resolve(SRC, rel), 'utf8');
const found: string[] = [];
let m: RegExpExecArray | null;
DECAY_LITERAL_RE.lastIndex = 0;
while ((m = DECAY_LITERAL_RE.exec(src)) !== null) {
found.push(m[1] ?? '');
}
if (found.length === 0) {
// orderbook files may legitimately have 0 if decay isn't used there anymore
ok(`${rel}: no inlined decay literal (fine)`);
continue;
}
totalLiterals += found.length;
const wrong = found.filter((n) => Number(n) !== REPUTATION_DECAY_HALF_LIFE_DAYS);
if (wrong.length === 0) {
ok(`${rel}: all ${found.length} inlined decay literal(s) = ${REPUTATION_DECAY_HALF_LIFE_DAYS}`);
} else {
mismatches += wrong.length;
bad(
`${rel}`,
`${wrong.length} inlined decay literal(s) are ${[...new Set(wrong)].join(',')}, expected ${REPUTATION_DECAY_HALF_LIFE_DAYS} (drift from the constant — see cp175 F-011)`
);
}
}
if (totalLiterals < 6) {
bad('discovery', `expected ≥6 inlined decay literals (feedback.ts alone has 6), found ${totalLiterals}. If the SQL was refactored onto reputationDecayWeightSql(), update/retire this sentinel.`);
} else if (mismatches === 0) {
ok(`all ${totalLiterals} inlined SQL decay literals across the API match the canonical constant`);
}
console.log('\n' + '─'.repeat(56));
if (fail === 0) {
console.log(`✓ all ${pass} scenarios passed`);
process.exit(0);
} else {
console.log(`${fail} of ${pass + fail} scenarios FAILED`);
process.exit(1);
}