diff --git a/lib/services/pax8-company-matcher.ts b/lib/services/pax8-company-matcher.ts index 248e334..0de0c19 100644 --- a/lib/services/pax8-company-matcher.ts +++ b/lib/services/pax8-company-matcher.ts @@ -186,7 +186,12 @@ export async function matchPax8Companies(opts?: { dryRun?: boolean; }): Promise { const startedAt = Date.now(); - const limit = opts?.limit ?? 1000; + // WR-02 default raised well above any near-term PAX8/Autotask company + // count (validated at 118 companies; generous headroom for growth) and + // the eligibility ORDER BY below rotates fairness so accumulating + // review-flagged rows can no longer permanently starve alphabetically + // later companies even if this limit is ever hit. + const limit = opts?.limit ?? 10000; const dryRun = opts?.dryRun ?? false; const result: Pax8CompanyMatchResult = { @@ -198,17 +203,29 @@ export async function matchPax8Companies(opts?: { }; try { + // WR-02: ORDER BY name is a static cursor — once flagged-for-review + // companies accumulate (their matched_at stays NULL indefinitely; see + // recordConflict), they keep re-occupying the same slots in every run's + // top-N by name, and once eligible rows exceed `limit`, everything + // sorting alphabetically after that point is starved. Ordering instead + // by "last time this row was actually considered" (the open review's + // detected_at, falling back to matched_at, falling back to the epoch + // for never-yet-scanned rows) rotates fairness: least-recently-attempted + // rows always sort first, so a permanently-open review row sinks behind + // any row that hasn't been reconsidered as recently. const eligible = await postgresClient.query( - `SELECT id::text, name - FROM pax8_companies - WHERE is_deleted = false - AND match_method IS DISTINCT FROM 'manual' + `SELECT c.id::text, c.name + FROM pax8_companies c + LEFT JOIN pax8_company_match_review r + ON r.pax8_company_id = c.id AND r.resolved_at IS NULL + WHERE c.is_deleted = false + AND c.match_method IS DISTINCT FROM 'manual' AND NOT EXISTS ( - SELECT 1 FROM pax8_company_match_review r - WHERE r.pax8_company_id = pax8_companies.id - AND r.resolved_at IS NOT NULL + SELECT 1 FROM pax8_company_match_review r2 + WHERE r2.pax8_company_id = c.id + AND r2.resolved_at IS NOT NULL ) - ORDER BY name + ORDER BY COALESCE(r.detected_at, c.matched_at, 'epoch'::timestamptz) ASC LIMIT $1`, [limit] );