← back to Ventura Corridor
src/jobs/normalize_categories.ts
73 lines
/**
* Backfill / fix category_tag on magazine_features using a deterministic
* pitch_type → category_tag mapping (instead of trusting qwen3's pick).
* Runs idempotently; only updates rows where the deterministic value differs.
*
* $ npx tsx src/jobs/normalize_categories.ts # apply to all
* $ npx tsx src/jobs/normalize_categories.ts --dry-run
*/
import 'dotenv/config';
import { pool, query } from '../../db/pool.ts';
const DRY = process.argv.includes('--dry-run');
const MAP: Record<string, string> = {
'restaurant': 'restaurant',
'food-service': 'restaurant',
'cafe': 'restaurant',
'bar': 'restaurant',
'salon-spa': 'salon',
'beauty-salon': 'salon',
'beauty-spa': 'beauty',
'nail-salon': 'beauty',
'medical-clinic': 'medical',
'dental-office': 'medical',
'chiropractic': 'medical',
'therapy': 'medical',
'fitness-studio': 'fitness',
'gym': 'fitness',
'yoga': 'fitness',
'law-office': 'professional',
'accountant': 'professional',
'cpa': 'professional',
'real-estate': 'real-estate',
'real-estate-agent': 'real-estate',
'realty': 'real-estate',
'retail-storefront': 'shop',
'boutique': 'shop',
'gift-shop': 'shop',
'office-tenant': 'professional',
'building-neighbor': 'professional',
'hospitality': 'hospitality',
'hotel': 'hospitality',
'automotive': 'automotive',
'auto-repair': 'automotive',
'car-wash': 'automotive',
'beauty': 'beauty',
};
async function main() {
const r = await query(`
SELECT mf.id, mf.category_tag, p.pitch_type
FROM magazine_features mf
JOIN pitches p ON p.business_id = mf.business_id
WHERE p.pitch_type IS NOT NULL
`);
let toUpdate = 0;
for (const row of r.rows) {
const expected = MAP[row.pitch_type];
if (!expected) continue;
if (row.category_tag === expected) continue;
if (DRY) {
console.log(` [dry] ${row.id} ${row.pitch_type} : ${row.category_tag} → ${expected}`);
} else {
await query(`UPDATE magazine_features SET category_tag = $1 WHERE id = $2`, [expected, row.id]);
}
toUpdate++;
}
console.log(`[normalize_categories] ${DRY ? 'would update' : 'updated'} ${toUpdate} of ${r.rowCount} features`);
await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });