SPB Git

spb/cancerindex

Public

Drug reconciliation: deterministic tie-break for aliases shared by salt-form duplicates

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 11, 2026) parent 3b30d3a Browse files

1 changed file +31 −4

Unified Split
modified packages/connectors/src/connectors/clinicaltrials/drugs.ts +31 −4
@@ -187,14 +187,41 @@ export async function reconcileInterventionDrugs(db: Database, opts: { remap?: b
187 if (opts.remap) { 187 if (opts.remap) {
188 await db.execute(sql`UPDATE trial_interventions SET drug_id = NULL, match_type = 'UNRESOLVED' WHERE intervention_type IN ('DRUG','BIOLOGICAL') AND match_type IN ('ALIAS','PROBABILISTIC')`); 188 await db.execute(sql`UPDATE trial_interventions SET drug_id = NULL, match_type = 'UNRESOLVED' WHERE intervention_type IN ('DRUG','BIOLOGICAL') AND match_type IN ('ALIAS','PROBABILISTIC')`);
189 } 189 }
190− // Unambiguous alias map: normalized → drug_id (aliases shared by several drugs are excluded). 190+ // Alias map: normalized → drug_id. An alias shared by several drugs is accepted only when one
191+ // candidate is strictly preferred by a deterministic rule (SPEC §97 duplicate resolution):
192+ // 0 the alias is the drug's own generic name (alias_type generic AND normalized(drugs.name) = alias)
193+ // 1 the alias is a brand of the drug
194+ // 2 the drug's name carries no salt/ester token (base molecule rather than "X Hydrochloride")
195+ // 3 anything else
196+ // Two candidates at the same best priority → ambiguous → skipped (never guessed). Salt-form
197+ // duplicates ("Erlotinib" vs "Erlotinib Hydrochloride") are also reported for the merge queue.
191 await db.execute(sql`CREATE TEMP TABLE IF NOT EXISTS _alias_map (normalized text PRIMARY KEY, drug_id varchar(32) NOT NULL)`); 198 await db.execute(sql`CREATE TEMP TABLE IF NOT EXISTS _alias_map (normalized text PRIMARY KEY, drug_id varchar(32) NOT NULL)`);
192 await db.execute(sql`TRUNCATE _alias_map`); 199 await db.execute(sql`TRUNCATE _alias_map`);
193 await db.execute(sql` 200 await db.execute(sql`
194 INSERT INTO _alias_map (normalized, drug_id) 201 INSERT INTO _alias_map (normalized, drug_id)
195− SELECT normalized, min(drug_id) FROM drug_aliases WHERE length(normalized) >= 3 202+ WITH cand AS (
196− GROUP BY normalized HAVING count(DISTINCT drug_id) = 1`); 203+ SELECT a.normalized, a.drug_id,
197− const [amb] = await db.execute<{ n: string }>(sql`SELECT count(*)::text AS n FROM (SELECT normalized FROM drug_aliases GROUP BY normalized HAVING count(DISTINCT drug_id) > 1) x`); 204+ min(CASE
205+ WHEN a.alias_type = 'generic' AND lower(regexp_replace(d.name, '[^A-Za-z0-9]+', ' ', 'g')) = a.normalized THEN 0
206+ WHEN a.alias_type = 'brand' THEN 1
207+ WHEN NOT (lower(d.name) ~ ${sql.raw(`'\\m(${SALT_TOKENS.join('|')})\\M'`)}) THEN 2
208+ ELSE 3 END) AS prio
209+ FROM drug_aliases a JOIN drugs d ON d.id = a.drug_id
210+ WHERE length(a.normalized) >= 3
211+ GROUP BY a.normalized, a.drug_id
212+ ),
213+ best AS (
214+ SELECT normalized, min(prio) AS prio FROM cand GROUP BY normalized
215+ ),
216+ pick AS (
217+ SELECT c.normalized, c.drug_id, count(*) OVER (PARTITION BY c.normalized) AS n_best
218+ FROM cand c JOIN best b ON b.normalized = c.normalized AND b.prio = c.prio
219+ )
220+ SELECT normalized, drug_id FROM pick WHERE n_best = 1`);
221+ const [amb] = await db.execute<{ n: string }>(sql`
222+ SELECT count(*)::text AS n FROM (
223+ SELECT a.normalized FROM drug_aliases a WHERE length(a.normalized) >= 3 GROUP BY a.normalized HAVING count(DISTINCT a.drug_id) > 1
224+ EXCEPT SELECT normalized FROM _alias_map) x`);
198 225
199 // Pass 1: exact normalized alias. 226 // Pass 1: exact normalized alias.
200 const p1 = await db.execute<{ n: string }>(sql` 227 const p1 = await db.execute<{ n: string }>(sql`
201 228
Menu

Theme