SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%

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 13 days ago (Sep 11, 2026) parent 3b30d3a

1 changed file +31 −4

modified packages/connectors/src/connectors/clinicaltrials/drugs.ts +31 −4
@@ -187,14 +187,41 @@ export async function reconcileInterventionDrugs(db: Database, opts: { remap?: b
187 187 if (opts.remap) {
188 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 198 await db.execute(sql`CREATE TEMP TABLE IF NOT EXISTS _alias_map (normalized text PRIMARY KEY, drug_id varchar(32) NOT NULL)`);
192 199 await db.execute(sql`TRUNCATE _alias_map`);
193 200 await db.execute(sql`
194 201 INSERT INTO _alias_map (normalized, drug_id)
195 SELECT normalized, min(drug_id) FROM drug_aliases WHERE length(normalized) >= 3
196 GROUP BY normalized HAVING count(DISTINCT drug_id) = 1`);
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`);
202 + WITH cand AS (
203 + SELECT a.normalized, a.drug_id,
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 226 // Pass 1: exact normalized alias.
200 227 const p1 = await db.execute<{ n: string }>(sql`
201 228