/** * One-off clean-up when quote matching became AI-only (2026-10-24). * * The retired rule-based matcher left soft suggestions (`potential_quote`) * on unlinked orders — many wrong: one quote was suggested to 16 unrelated * orders because same company + customer scored 0.5 with no matching * lines. This clears every suggestion that did NOT come from the AI order * assistant (`quote_match_method` not starting with `ai_`), so the order * page shows "Match with AI" instead of a stale guess. * * Hard links (`orders.quote`) are never touched. AI suggestions stay. * Dry run unless `--apply`. * * npx tsx scripts/2026-10-24-clear-rule-quote-suggestions.ts [--apply] */ import "./_env"; import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; import { runWithAuditActor } from "../src/lib/server/audit-context"; const APPLY = process.argv.includes("--apply"); const actor = { id: "claude-quote-suggestion-clear", name: "Claude (rule-based suggestion clean-up)" }; const FILTER = 'potential_quote != "" && quote = "" && quote_match_method !~ "ai_"'; interface Row { id: string; order_number?: string; potential_quote?: string; potential_quote_confidence?: number; quote_match_method?: string; } async function main() { const pb = await getAdminPocketBase(); pb.autoCancellation(false); const rows = await pb.collection("orders").getFullList({ filter: FILTER, fields: "id,order_number,potential_quote,potential_quote_confidence,quote_match_method", sort: "-created", batch: 500, }); console.log(`${APPLY ? "APPLY" : "DRY "} ${rows.length} rule-based suggestion(s) on unlinked orders`); const perQuote = new Map(); const perMethod = new Map(); for (const r of rows) { perQuote.set(r.potential_quote || "", (perQuote.get(r.potential_quote || "") ?? 0) + 1); perMethod.set(r.quote_match_method || "(none)", (perMethod.get(r.quote_match_method || "(none)") ?? 0) + 1); } console.log("\n── by method ──"); for (const [m, n] of [...perMethod].sort((a, b) => b[1] - a[1])) console.log(` ${n} × ${m}`); console.log("\n── most-suggested quotes ──"); for (const [q, n] of [...perQuote].sort((a, b) => b[1] - a[1]).slice(0, 10)) console.log(` ${n} × ${q}`); if (!APPLY) { console.log("\nDry run — nothing was written. Re-run with --apply to clear them."); return; } let cleared = 0; let failed = 0; for (const r of rows) { try { await pb.collection("orders").update(r.id, { potential_quote: "", potential_quote_confidence: 0, quote_match_method: "", quote_matching_attempted: false, }); cleared++; } catch (err) { failed++; console.error(`ERROR ${r.order_number || r.id}: ${err instanceof Error ? err.message : err}`); } } console.log(`\ncleared ${cleared}, failed ${failed}`); } runWithAuditActor({ ...actor, source: "server", ignoreRequestUser: true }, main).catch((err) => { console.error(err); process.exit(1); });