Skip to content

Commit d417e2a

Browse files
mpstatonclaude
andcommitted
fix(crm-starter-export): deal-tolerant matcher, audited — trims and prefixes with guardrails
The tracker names deals, not orgs; the matcher now tolerates the decorations (slash-splits, ': <date>' suffixes, progressive right-trim with exact-match-only acceptance, unique-prefix rescue). The fuzzy-audit pass caught one real false positive — bare "james" (5 chars) prefix-matched james-and-judith-k-dimon-foundation — so single-token prefixes now need brand-length distinctiveness (≥8 chars: bloomberg matches, first names don't) and multi-token ≥5. Final run: 75/96 pipeline rows matched (29 exact + 46 fuzzy, all flagged for operator review), 21 to the sidecar as the operator's triage list. Alias-driven oddities (jed-foundation carries alias "todd-fisher") left untouched and flagged — canonical-layer data is not the matcher's to edit. Files changed: - scripts/export-crm-orgs-csv.mjs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW28dw3kQAKXr2ZNefCukE
1 parent 87f6e0c commit d417e2a

1 file changed

Lines changed: 44 additions & 1 deletion

File tree

scripts/export-crm-orgs-csv.mjs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,55 @@ for (const o of orgs) {
172172
if (n && !byNorm.has(n)) byNorm.set(n, o.slug);
173173
}
174174
}
175+
// Deal-decoration-tolerant fuzzy match. Tracker rows name DEALS, not orgs
176+
// ("Ballmer Group II", "ECMC-2", "Colorado League of Charter Schools:
177+
// 12/31/25", "Stand Together Foundation-catalyst grant"). Candidates tried
178+
// in order of confidence; everything found here is flagged 'fuzzy' for
179+
// operator review — false negatives beat false positives, so trimmed
180+
// variants must EXACT-match, and the last-resort prefix rule only fires
181+
// when it is unambiguous (exactly one org).
182+
const orgNorms = Array.from(byNorm.keys());
183+
const fuzzyMatch = (name) => {
184+
const variants = new Set();
185+
const base = normName(name);
186+
if (base) variants.add(base);
187+
for (const part of String(name).split('/')) {
188+
const n = normName(part);
189+
if (n) variants.add(n);
190+
}
191+
variants.add(normName(String(name).replace(/:.*$/, ''))); // ": 12/31/25"
192+
// Progressive right-trim (up to 3 trailing tokens): "stand together
193+
// foundation catalyst grant" → … → "stand together foundation".
194+
for (const v of Array.from(variants)) {
195+
const toks = v.split(' ');
196+
for (let drop = 1; drop <= 3 && toks.length - drop >= 1; drop += 1) {
197+
variants.add(toks.slice(0, toks.length - drop).join(' '));
198+
}
199+
}
200+
for (const v of variants) {
201+
if (byNorm.has(v)) return byNorm.get(v);
202+
}
203+
// Unique-prefix rescue: "bloomberg" → "bloomberg philanthropies" (only if
204+
// exactly one org starts with the variant). Single-token variants need
205+
// brand-length distinctiveness (≥8 chars) — a bare first name like
206+
// "james" (5) prefix-matched james-and-judith-k-dimon-foundation on the
207+
// first run (caught in the 2026-07-28 fuzzy audit); multi-token variants
208+
// need ≥5 chars.
209+
for (const v of variants) {
210+
const minLen = v.includes(' ') ? 5 : 8;
211+
if (v.length < minLen) continue;
212+
const hits = orgNorms.filter((n) => n.startsWith(v + ' '));
213+
if (hits.length === 1) return byNorm.get(hits[0]);
214+
}
215+
return null;
216+
};
217+
175218
// Per-PIPELINE-row resolution — multiple rows may share one org (multi-deal
176219
// orgs like Accelerate the Future); each keeps its own row, tracker-style.
177220
const resolved = pipelineRows.map((row) => {
178221
const name = row['Prospect / Organization'] ?? '';
179222
const exact = row.corpus_funder_slug && orgBySlug.has(row.corpus_funder_slug) ? row.corpus_funder_slug : null;
180-
const fuzzy = exact ? null : byNorm.get(normName(name));
223+
const fuzzy = exact ? null : fuzzyMatch(name);
181224
return { row, slug: exact ?? fuzzy ?? null, matched: exact ? 'exact' : fuzzy ? 'fuzzy' : 'none' };
182225
});
183226
const matchedCount = resolved.filter((r) => r.slug).length;

0 commit comments

Comments
 (0)