/** * GTM normalization primitives used by generated plays and user-authored * prospecting notebooks. * * Keep these helpers small, deterministic, and schema-shaped. They are not an * agent or a provider policy layer; they are the shared vocabulary for turning * messy GTM rows into contact/account fields that Deepline plays understand. * As provider enums and row conventions get sharper, improve them here so * bootstrapped plays get better without carrying more generated glue. * * Do not add helpers that extract values from tool/play results. Result values * should come from declared getters (`extractedValues.email.get()`) or declared * play output fields (`result.email`). Helpers in this file normalize input * rows and candidate entities, not opaque execution envelopes. */ export type GtmRow = Record; export type GtmContact = GtmRow & { first_name: string; last_name: string; domain: string; company_name: string; linkedin_url: string; title: string; email: string; phone: string; }; export function normalizeGtmRows(value: unknown): GtmRow[] { if (!value) return []; if (Array.isArray(value)) return value.filter(isRecord); if (isRecord(value)) { for (const key of ['rows', 'people', 'contacts', 'companies', 'data']) { const nested = value[key]; if (Array.isArray(nested)) return nested.filter(isRecord); } } return []; } export function normalizeGtmContact(row: GtmRow): GtmContact { const first = pickString(row, [ 'first_name', 'FIRST_NAME', 'firstName', 'first', 'firstname', 'First Name', 'given_name', 'givenName', ]); const last = pickString(row, [ 'last_name', 'LAST_NAME', 'lastName', 'last', 'lastname', 'Last Name', 'family_name', 'familyName', ]); const full = pickString(row, [ 'full_name', 'FULL_NAME', 'name', 'person_name', ]); const split = splitName(full); return { ...row, first_name: first || split.first, last_name: last || split.last, domain: pickString(row, [ 'domain', 'DOMAIN', 'company_domain', 'COMPANY_DOMAIN', 'companyDomain', 'website_domain', 'company website', 'Company Domain', 'Website', 'website', ]), company_name: pickString(row, [ 'company_name', 'COMPANY_NAME', 'company', 'organization_name', 'account_name', 'Company', 'Account', ]), linkedin_url: pickString(row, [ 'linkedin_url', 'LINKEDIN_URL', 'linkedin', 'person_linkedin_url', 'linkedin_profile', 'LinkedIn', ]), title: pickString(row, [ 'title', 'TITLE', 'job_title', 'headline', 'Job Title', ]), email: pickString(row, [ 'email', 'EMAIL', 'work_email', 'workEmail', 'Work Email', ]), phone: pickString(row, [ 'phone', 'PHONE', 'phone_number', 'phoneNumber', 'mobile', 'Mobile Phone', ]), }; } function isRecord(value: unknown): value is GtmRow { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); } function pickString(row: GtmRow, names: string[]): string { for (const name of names) { const value = row[name]; if (typeof value === 'string' && value.trim()) return value.trim(); } return ''; } function splitName(value: string): { first: string; last: string } { const parts = String(value || '') .trim() .split(/\s+/) .filter(Boolean); return { first: parts[0] ?? '', last: parts.slice(1).join(' ') }; }