@ansvar/singapore-law-mcp
Version:
Complete Singapore law database — 523 Acts, 28K+ provisions from Singapore Statutes Online (sso.agc.gov.sg) with full-text search, definitions, and citation support
148 lines • 5.46 kB
JavaScript
/**
* Singaporean Law MCP — Data Freshness Checker
*
* Checks whether the local database is stale or missing expected legislation.
*
* Detection strategy:
* 1. Database age — flags if build_date > MAX_AGE days old
* 2. Document count — compares DB rows against census.json expected count
* 3. Source portal — verifies the official legal portal is reachable
*
* Exit codes:
* 0 = database is fresh, no updates detected
* 1 = updates detected (stale DB, missing documents, or new content upstream)
* 2 = check failed (DB missing, portal unreachable, unexpected error)
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_PATH = resolve(__dirname, '../data/database.db');
const CENSUS_PATH = resolve(__dirname, '../data/census.json');
const MAX_DB_AGE_DAYS = Number(process.env['MAX_DB_AGE_DAYS'] ?? '90');
const PORTAL_URL = 'https://sso.agc.gov.sg';
const PORTAL_NAME = 'Singapore Statutes Online';
function daysSince(isoDate) {
const dt = new Date(isoDate);
if (Number.isNaN(dt.getTime()))
return null;
return Math.floor((Date.now() - dt.getTime()) / (1000 * 60 * 60 * 24));
}
async function checkPortal(url) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
const res = await fetch(url, {
method: 'HEAD',
signal: controller.signal,
headers: { 'User-Agent': '@ansvar/singaporean-law-mcp/1.0 (data-freshness-check)' },
});
clearTimeout(timeout);
return res.ok || res.status === 301 || res.status === 302 || res.status === 403;
}
catch {
return false;
}
}
async function main() {
console.log('Singaporean Law MCP — Data Freshness Check');
console.log(`Portal: ${PORTAL_NAME} (${PORTAL_URL})`);
console.log('');
// --- 1. Database existence ---
if (!existsSync(DB_PATH)) {
console.error('ERROR: Database not found at', DB_PATH);
console.error('Run "npm run build:db" first.');
process.exit(2);
}
// --- 2. Database age check ---
let updatesNeeded = false;
const { default: Database } = await import('@ansvar/mcp-sqlite');
const db = new Database(DB_PATH, { readonly: true });
let buildDate = null;
try {
const row = db.prepare("SELECT value FROM db_metadata WHERE key = 'build_date'").get();
buildDate = row?.value ?? null;
}
catch {
// db_metadata table may not exist
}
if (buildDate) {
const age = daysSince(buildDate);
if (age !== null && age > MAX_DB_AGE_DAYS) {
console.log(`STALE: Database is ${age} days old (threshold: ${MAX_DB_AGE_DAYS} days)`);
updatesNeeded = true;
}
else if (age !== null) {
console.log(`OK: Database is ${age} days old (threshold: ${MAX_DB_AGE_DAYS} days)`);
}
}
else {
console.log('WARN: No build_date in db_metadata — cannot assess age');
}
// --- 3. Document count check ---
let dbDocCount = 0;
let dbProvCount = 0;
try {
const docRow = db.prepare("SELECT COUNT(*) as count FROM legal_documents").get();
dbDocCount = docRow.count;
console.log(`DB documents: ${dbDocCount}`);
}
catch {
console.log('WARN: Cannot count legal_documents');
}
try {
const provRow = db.prepare("SELECT COUNT(*) as count FROM legal_provisions").get();
dbProvCount = provRow.count;
console.log(`DB provisions: ${dbProvCount}`);
}
catch {
console.log('WARN: Cannot count legal_provisions');
}
// Compare against census if available
if (existsSync(CENSUS_PATH)) {
try {
const census = JSON.parse(readFileSync(CENSUS_PATH, 'utf-8'));
const expected = census.summary?.total_ingested ?? census.summary?.total_ingestable ?? census.summary?.total_laws;
if (expected && dbDocCount < expected) {
console.log(`MISSING: DB has ${dbDocCount} documents but census expects ${expected}`);
updatesNeeded = true;
}
else if (expected) {
console.log(`OK: DB documents (${dbDocCount}) >= census expected (${expected})`);
}
}
catch {
console.log('WARN: Could not parse census.json');
}
}
else {
console.log('INFO: No census.json — skipping count comparison');
}
db.close();
// --- 4. Source portal reachability ---
console.log('');
console.log(`Checking portal: ${PORTAL_URL}`);
const portalOk = await checkPortal(PORTAL_URL);
if (portalOk) {
console.log(`OK: ${PORTAL_NAME} is reachable`);
}
else {
console.log(`WARN: ${PORTAL_NAME} is unreachable — manual check recommended`);
}
// --- Result ---
console.log('');
if (updatesNeeded) {
console.log('RESULT: Updates detected — re-ingestion recommended');
process.exit(1);
}
else {
console.log('RESULT: Database appears current — no updates needed');
process.exit(0);
}
}
main().catch((err) => {
console.error('Unexpected error:', err);
process.exit(2);
});
//# sourceMappingURL=check-updates.js.map